3 Commits

Author SHA1 Message Date
LukeMackin
6cd2a22d09 feat(expo): app.json 添加 runtimeVersion 字段 2026-07-20 00:38:32 +08:00
LukeMackin
71f6b6369b fix: expo-manifest.json 改为 Expo Updates Modern Manifest 格式(id+createdAt+runtimeVersion) 2026-07-19 23:46:17 +08:00
LukeMackin
e5ce52ac40 feat(expo): JS热更新(expo-updates) + APK大更新双轨
- app.json: expo-updates配置(checkAutomatically ON_LOAD)
- ota/expo-manifest.json: JS bundle热更新manifest
- App.tsx: 启动时自动检查JS+APK双轨更新
- pnpm install后npx expo prebuild生成android/原生工程
2026-07-19 23:40:17 +08:00
130 changed files with 1320 additions and 10507 deletions

7
.gitignore vendored
View File

@@ -47,10 +47,3 @@ android/build/
local.properties local.properties
screen.png screen.png
# Temp/debug files
*.traineddata
tessdata_temp/
*.ps1
*_temp/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

View File

@@ -1,2 +0,0 @@
{"status":"starting_turn","iteration":0,"created_at":"2026-07-20T16:59:22.3318065Z"}
{"status":"turn_done","iteration":1,"created_at":"2026-07-20T16:59:51.4961797Z"}

View File

@@ -1,9 +0,0 @@
[
{
"fingerprint": "15-127-0-0-1-10808-android-studio-sync-project-7bf5ff43",
"summary": "代理是根因15 个失败都是因为 `127.0.0.1:10808` 代理)。已移除。在 Android Studio 里点 **大象图标**Sync Project重新同步",
"first_seen_iteration": 1,
"last_seen_iteration": 1,
"count": 1
}
]

View File

@@ -1,9 +0,0 @@
{
"status": "running",
"iteration": 1,
"current_direction": "代理是根因15 个失败都是因为 `127.0.0.1:10808` 代理)。已移除。在 Android Studio 里点 **大象图标**Sync Project重新同步",
"stale_count": 1,
"pivot_count": 0,
"blocked_reason": "",
"updated_at": "2026-07-20T16:59:51.4929941Z"
}

View File

@@ -1,28 +0,0 @@
{"status":"starting_turn","iteration":0,"created_at":"2026-07-21T03:16:15.2382547Z"}
{"status":"turn_done","iteration":1,"created_at":"2026-07-21T03:17:05.8410329Z"}
{"status":"starting_turn","iteration":1,"created_at":"2026-07-21T03:17:06.1408318Z"}
{"status":"turn_done","iteration":2,"created_at":"2026-07-21T03:17:34.0830711Z"}
{"status":"starting_turn","iteration":2,"created_at":"2026-07-21T03:17:34.8204313Z"}
{"status":"turn_done","iteration":3,"created_at":"2026-07-21T03:18:32.1678783Z"}
{"status":"starting_turn","iteration":3,"created_at":"2026-07-21T03:18:32.1884204Z"}
{"status":"turn_done","iteration":4,"created_at":"2026-07-21T03:19:08.6392005Z"}
{"status":"starting_turn","iteration":4,"created_at":"2026-07-21T03:19:08.7309788Z"}
{"status":"turn_done","iteration":5,"created_at":"2026-07-21T03:19:33.0643356Z"}
{"status":"starting_turn","iteration":5,"created_at":"2026-07-21T03:19:33.2245358Z"}
{"status":"turn_done","iteration":6,"created_at":"2026-07-21T03:20:56.4286355Z"}
{"status":"starting_turn","iteration":6,"created_at":"2026-07-21T03:20:56.5224447Z"}
{"status":"turn_done","iteration":7,"created_at":"2026-07-21T03:22:00.7927224Z"}
{"status":"starting_turn","iteration":7,"created_at":"2026-07-21T03:22:00.8862815Z"}
{"status":"turn_done","iteration":8,"created_at":"2026-07-21T03:22:46.0575234Z"}
{"status":"starting_turn","iteration":8,"created_at":"2026-07-21T03:22:46.1718312Z"}
{"status":"turn_done","iteration":9,"created_at":"2026-07-21T03:24:13.7257985Z"}
{"status":"starting_turn","iteration":9,"created_at":"2026-07-21T03:24:13.8277354Z"}
{"status":"turn_done","iteration":10,"created_at":"2026-07-21T03:24:27.5537437Z"}
{"status":"starting_turn","iteration":10,"created_at":"2026-07-21T03:24:27.6571627Z"}
{"status":"turn_done","iteration":11,"created_at":"2026-07-21T03:25:19.3030817Z"}
{"status":"starting_turn","iteration":11,"created_at":"2026-07-21T03:25:19.4076559Z"}
{"status":"turn_done","iteration":12,"created_at":"2026-07-21T03:29:18.0058775Z"}
{"status":"starting_turn","iteration":12,"created_at":"2026-07-21T03:29:18.1383267Z"}
{"status":"turn_done","iteration":13,"created_at":"2026-07-21T03:30:01.3929977Z"}
{"status":"starting_turn","iteration":13,"created_at":"2026-07-21T03:30:01.5037583Z"}
{"status":"turn_done","iteration":14,"created_at":"2026-07-21T03:30:14.626083Z"}

View File

@@ -1,100 +0,0 @@
[
{
"fingerprint": "as-build-ad2624ab",
"summary": "已修复两个配置。在 AS 里重新 Build",
"first_seen_iteration": 1,
"last_seen_iteration": 1,
"count": 1
},
{
"fingerprint": "as-build-20a5fde3",
"summary": "在 AS 重新 Build。这两步被彻底跳过。",
"first_seen_iteration": 2,
"last_seen_iteration": 2,
"count": 1
},
{
"fingerprint": "task",
"summary": "---",
"first_seen_iteration": 3,
"last_seen_iteration": 3,
"count": 1
},
{
"fingerprint": "android-studio-build-release-expo-updates-root-8e271660",
"summary": "用 Android Studio 重新 Build Release。修复了 `expo-updates` 路径加倍问题(缺少 `root` 配置项):",
"first_seen_iteration": 4,
"last_seen_iteration": 4,
"count": 1
},
{
"fingerprint": "root-createreleaseupdatesresources-packages-client-andro-c0afb42a",
"summary": "`root` 设定没有解决。`createReleaseUpdatesResources` 仍然报双重路径 `packages\\client-android\\packages\\client-android\\App.tsx`——这个任务不",
"first_seen_iteration": 5,
"last_seen_iteration": 5,
"count": 1
},
{
"fingerprint": "task-5b2466d1",
"summary": "两个修复就位:",
"first_seen_iteration": 6,
"last_seen_iteration": 6,
"count": 1
},
{
"fingerprint": "android-studio-build-release-c00a91b0",
"summary": "**现在请用 Android Studio 重新 Build Release。**",
"first_seen_iteration": 7,
"last_seen_iteration": 7,
"count": 1
},
{
"fingerprint": "android-studio-build-release-7002dec6",
"summary": "**请用 Android Studio 重新 Build Release。**",
"first_seen_iteration": 8,
"last_seen_iteration": 8,
"count": 1
},
{
"fingerprint": "build-gradle-afterevaluate-js-bundle-ae4afdad",
"summary": "`build.gradle` 已清理为干净的单一 `afterEvaluate` 块。JS bundle 已就位。",
"first_seen_iteration": 9,
"last_seen_iteration": 9,
"count": 1
},
{
"fingerprint": "stale-9-tasks-named-configure-enabled-false-afterevaluat-49c894d6",
"summary": "stale=9。`tasks.named().configure { enabled = false }` 在 `afterEvaluate` 中都无效——这两个任务由 Gradle 插件在配置阶段强制注册<EFBC8C>",
"first_seen_iteration": 10,
"last_seen_iteration": 10,
"count": 1
},
{
"fingerprint": "android-studio-build-release-0ac7a51a",
"summary": "**现在用 Android Studio 直接 Build Release——不用加任何参数。**",
"first_seen_iteration": 11,
"last_seen_iteration": 11,
"count": 1
},
{
"fingerprint": "redmi-usb-79ba1bcf",
"summary": "卸载成功。安装受阻——Redmi USB 安装限制。**请解锁手机屏幕,点\"允许\"后告诉我。**",
"first_seen_iteration": 12,
"last_seen_iteration": 12,
"count": 1
},
{
"fingerprint": "android-studio-feat-expo-ota-12-main-apk-usb-45c0be45",
"summary": "Android Studio 里的项目还是 `feat/expo-ota`(所以 12 次失败)。我这边 main 的 APK 已经好了——安装不过去是手机 USB 权限。",
"first_seen_iteration": 13,
"last_seen_iteration": 13,
"count": 1
},
{
"fingerprint": "13-autoresearch-bafea58b",
"summary": "**结论已定。** 13轮迭代证据充分。关闭 AutoResearch",
"first_seen_iteration": 14,
"last_seen_iteration": 14,
"count": 1
}
]

View File

@@ -1,9 +0,0 @@
{
"status": "complete",
"iteration": 14,
"current_direction": "**结论已定。** 13轮迭代证据充分。关闭 AutoResearch",
"stale_count": 14,
"pivot_count": 1,
"blocked_reason": "",
"updated_at": "2026-07-21T03:30:14.7277324Z"
}

View File

@@ -1,2 +0,0 @@
{"status":"starting_turn","iteration":0,"created_at":"2026-07-21T03:15:01.5344339Z"}
{"status":"turn_done","iteration":1,"created_at":"2026-07-21T03:16:14.9716471Z"}

View File

@@ -1,9 +0,0 @@
[
{
"fingerprint": "910kb-bundle-android-studio-build-generate-signed-bundle-0bdca4e3",
"summary": "910KB bundle 已放好。Android Studio 里重新 Build → Generate Signed Bundle / APK → release → Finish。这次不会超时也不会 Metro 报错。",
"first_seen_iteration": 1,
"last_seen_iteration": 1,
"count": 1
}
]

View File

@@ -1,9 +0,0 @@
{
"status": "running",
"iteration": 1,
"current_direction": "910KB bundle 已放好。Android Studio 里重新 Build → Generate Signed Bundle / APK → release → Finish。这次不会超时也不会 Metro 报错。",
"stale_count": 1,
"pivot_count": 0,
"blocked_reason": "",
"updated_at": "2026-07-21T03:16:14.9681754Z"
}

53
analyze_text.py Normal file
View File

@@ -0,0 +1,53 @@
"""
Final analysis: Search for any readable content in the decoded text.
Look for patterns that might indicate it's a screenshot from an Android device.
"""
import sys
import re
path = r"D:\Yuzu-GCA\screen.png"
with open(path, 'rb') as f:
raw = f.read()
text = raw.decode('utf-16-le')
# Save full text for manual inspection
with open(r"D:\Yuzu-GCA\screen_full_text.txt", 'w', encoding='utf-8') as f:
f.write(text)
print("Full text written to screen_full_text.txt")
# Search for any CJK character sequences (potential readable Chinese text)
# Chinese characters are in range U+4E00 to U+9FFF
cjk_pattern = re.compile(r'[\u4e00-\u9fff]{3,}')
cjk_matches = cjk_pattern.findall(text)
print(f"\nChinese character sequences (>=3 chars): {len(cjk_matches)}")
# Show unique ones
unique_cjk = list(set(cjk_matches))
print(f"Unique sequences: {len(unique_cjk)}")
for seq in sorted(unique_cjk, key=len, reverse=True)[:30]:
print(f" '{seq}' (len={len(seq)})")
# Search for any ASCII word sequences (potential English words)
# Words of length >= 4 made of letters only
word_pattern = re.compile(r'[A-Za-z]{4,}')
word_matches = word_pattern.findall(text)
print(f"\nEnglish word-like sequences (>=4 chars): {len(word_matches)}")
unique_words = list(set(word_matches))
print(f"Unique: {len(unique_words)}")
for w in sorted(unique_words, key=len, reverse=True)[:40]:
print(f" '{w}'")
# Also search for numbers that look like versions
version_pattern = re.compile(r'\d+\.\d+(\.\d+)?')
version_matches = version_pattern.findall(text)
print(f"\nVersion-like patterns: {len(version_matches)}")
for v in version_matches[:20]:
print(f" '{v}'")
# Search for URLs or file paths
path_pattern = re.compile(r'[/\\][A-Za-z0-9_./\\-]{5,}')
path_matches = path_pattern.findall(text)
print(f"\nPath-like patterns: {len(path_matches)}")
for p in path_matches[:20]:
print(f" '{p}'")

View File

@@ -1,35 +0,0 @@
const http = require('http');
const WebSocket = require('ws');
http.get('http://localhost:9223/json', res => {
let d = '';
res.on('data', c => d += c);
res.on('end', () => {
const pages = JSON.parse(d);
const wsUrl = pages[0].webSocketDebuggerUrl;
const ws = new WebSocket(wsUrl);
ws.on('open', () => {
ws.send(JSON.stringify({id: 1, method: 'Runtime.evaluate', params: {expression: 'document.getElementById("ver").innerText'}}));
});
ws.on('message', data => {
try {
const msg = JSON.parse(data);
if (msg.id === 1) {
console.log('VERSION:', msg.result?.result?.value ?? JSON.stringify(msg.result));
ws.send(JSON.stringify({id: 2, method: 'Runtime.evaluate', params: {expression: 'localStorage.getItem("gca_ver") || "none"'}}));
}
if (msg.id === 2) {
console.log('LOCALSTORAGE gca_ver:', msg.result?.result?.value ?? JSON.stringify(msg.result));
ws.send(JSON.stringify({id: 3, method: 'Runtime.evaluate', params: {expression: 'document.getElementById("log").innerText'}}));
}
if (msg.id === 3) {
console.log('LOG:', msg.result?.result?.value?.substring(0, 500) ?? JSON.stringify(msg.result));
ws.close();
process.exit(0);
}
} catch(e) {}
});
});
}).on('error', e => { console.log('HTTP ERR:', e.message); process.exit(1); });
setTimeout(() => process.exit(0), 8000);

17
check_files.py Normal file
View File

@@ -0,0 +1,17 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# Check screen2.png
data = open(r"D:\Yuzu-GCA\screen2.png", 'rb').read()
print(f"screen2.png: Size={len(data)}")
print(f"First 20 bytes: {' '.join(f'{b:02X}' for b in data[:20])}")
print(f"Is valid PNG: {data[:8] == b'\x89PNG\r\n\x1a\n'}")
# Also check the fixed files
for fname in ['screen_fixed.png', 'screen_fixed2.png']:
try:
d = open(r"D:\\Yuzu-GCA\\" + fname, 'rb').read()
print(f"\n{fname}: Size={len(d)}")
except:
print(f"\n{fname}: NOT FOUND")

36
check_header.py Normal file
View File

@@ -0,0 +1,36 @@
import sys
image_path = r"D:\Yuzu-GCA\screen.png"
with open(image_path, 'rb') as f:
header = f.read(32)
print(f"File size check: first 32 bytes hex:")
print(' '.join(f'{b:02X}' for b in header))
print()
print(f"ASCII representation: {header}")
# Check known magic numbers
if header[:8] == b'\x89PNG\r\n\x1a\n':
print("✓ Valid PNG header detected")
elif header[:2] == b'\xff\xd8':
print("✓ JPEG header detected")
elif header[:4] == b'BM':
print("✓ BMP header detected")
elif header[:4] == b'RIFF':
print("✓ WEBP header detected")
else:
print("⚠ Unknown image format")
# Also check if it might be an Android screencap (could be raw)
# Try to detect if it's a PNG embedded in a wrapper
print()
# Check if file is actually a text-based format
with open(image_path, 'rb') as f:
content = f.read(200)
# Check if mostly printable ASCII
printable = sum(1 for b in content if 32 <= b <= 126 or b in (9, 10, 13))
if printable > len(content) * 0.8:
print("Note: File appears to be mostly text!")
print(content.decode('utf-8', errors='replace'))

74
check_png.py Normal file
View File

@@ -0,0 +1,74 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'rb') as f:
data = f.read()
print(f"Total size: {len(data)}")
# Check PNG structure
# Signature: 8 bytes
# Then chunks: each chunk has 4-byte length, 4-byte type, data, 4-byte CRC
offset = 8 # skip signature
while offset < len(data):
if offset + 8 > len(data):
break
length = int.from_bytes(data[offset:offset+4], 'big')
chunk_type = data[offset+4:offset+8].decode('ascii', errors='replace')
print(f"Chunk at offset {offset}: type='{chunk_type}', length={length}")
if chunk_type == 'IHDR':
w = int.from_bytes(data[offset+8:offset+12], 'big')
h = int.from_bytes(data[offset+12:offset+16], 'big')
bit_depth = data[offset+16]
color_type = data[offset+17]
print(f" Width={w}, Height={h}, BitDepth={bit_depth}, ColorType={color_type}")
# Height seems wrong (854591). Expected ~1920 or ~2340
# 0x000D0A3F: maybe it was 0x00000780 (1920) or 0x00000924 (2340)?
# Let me look at the raw bytes around height
print(f" Raw height bytes: {' '.join(f'{b:02X}' for b in data[offset+12:offset+16])}")
# Expected height around 1920 = 0x780, or 2340 = 0x924
# What we have: 00 0D 0A 3F = 0x000D0A3F
# Could it be that 0D 0A is an inserted CR LF?
# If we remove 0D 0A: remaining would be 00 3F... but that's only 2 bytes
# Actually: 00 [0D 0A] 3F -> 00 3F? That's too short.
# Maybe: 00 [0D] 0A 3F, and 0D was inserted? Then original: 00 0A 3F = 2623 pixels?
# Or maybe: 00 00 0A 3F was original? No.
# Let me check: common heights: 1920=0x780, 2000=0x7D0, 2340=0x924
# 0x0A3F = 2623 (close to nothing standard)
# What if the actual height bytes are 00 00 0A 3F? That's still 2623.
# What about 00 00 07 80 (1920)? Then 0D 0A replaced 00 07 somehow?
offset += 12 + length
if offset > 200:
break
# Let me search for CR LF (0D 0A) in the PNG data
crlf_count = 0
for i in range(len(data) - 1):
if data[i] == 0x0D and data[i+1] == 0x0A:
crlf_count += 1
if crlf_count <= 10:
print(f"CR LF at offset {i}: context: {' '.join(f'{b:02X}' for b in data[max(0,i-4):i+6])}")
print(f"\nTotal CR LF sequences: {crlf_count}")
# Also count lone 0A
lone_lf = 0
for i in range(len(data)):
if data[i] == 0x0A and (i == 0 or data[i-1] != 0x0D):
lone_lf += 1
print(f"Lone LF: {lone_lf}")
# Count lone 0D
lone_cr = 0
for i in range(len(data)):
if data[i] == 0x0D and (i == len(data)-1 or data[i+1] != 0x0A):
lone_cr += 1
print(f"Lone CR: {lone_cr}")

1
chi_sim.traineddata Normal file
View File

@@ -0,0 +1 @@
File size exceeded the configured limit of 20 MB.

82
debug_png.py Normal file
View File

@@ -0,0 +1,82 @@
"""
Debug: compare raw bytes and re-encoded bytes.
Also try to interpret the file as a regular PNG by skipping the BOM.
"""
import sys
path = r"D:\Yuzu-GCA\screen.png"
with open(path, 'rb') as f:
raw = f.read()
print(f"Total size: {len(raw)} bytes")
# Decode as UTF-16 LE
text = raw.decode('utf-16-le')
print(f"Decoded text length: {len(text)} chars")
# Re-encode
reencoded = text.encode('utf-16-le')
print(f"Re-encoded size: {len(reencoded)} bytes")
# Compare
if raw == reencoded:
print("raw == reencoded: EXACT MATCH")
else:
print(f"raw == reencoded: DIFFER")
# Find first difference
for i in range(min(len(raw), len(reencoded))):
if raw[i] != reencoded[i]:
print(f"First diff at byte {i}: raw={raw[i]:02x}, reencoded={reencoded[i]:02x}")
print(f" Context raw: {raw[max(0,i-5):i+10].hex(' ')}")
print(f" Context re: {reencoded[max(0,i-5):i+10].hex(' ')}")
break
# Check if raw starts with BOM
if raw[:2] == b'\xff\xfe':
print("\nFile starts with UTF-16 LE BOM")
elif raw[:2] == b'\xfe\xff':
print("\nFile starts with UTF-16 BE BOM")
# Check reencoded start
if reencoded[:2] == b'\xff\xfe':
print("Re-encoded starts with UTF-16 LE BOM")
elif reencoded[:2] == b'\xfe\xff':
print("Re-encoded starts with UTF-16 BE BOM")
# Try: what if we treat raw[2:] as the UTF-16 LE data (without the BOM)?
# This would be the case if the file had BOM added later
print(f"\nraw[2:4] = {raw[2:4].hex(' ')}")
text2 = raw[2:].decode('utf-16-le')
print(f"Decoded text2 (skipping BOM) length: {len(text2)} chars")
print(f"text2 first 50 chars: {text2[:50]}")
# Try: what if the entire file is just raw PNG with a bogus BOM?
# PNG magic: 89 50 4E 47 0D 0A 1A 0A
# Our file: ff fe 52 58 4e 00 47 00 0d 00 0a 00 ...
# Maybe each byte X became X*256 + something?
# Or maybe the file is a hex dump?
print("\n=== Trying hex dump interpretation ===")
# Check if the text looks like a hex dump (pairs of hex digits)
hex_pattern = all(c in '0123456789abcdefABCDEF \n\r\t' for c in text[:1000])
print(f"First 1000 chars look like hex dump: {hex_pattern}")
# Final: Is there any actual readable content?
# Let's look for sequences of Latin chars that might form words
import re
# Words with 5+ letters
real_words = re.findall(r'[A-Za-z]{5,}', text)
# Filter out those that look like random binary
likely_real = [w for w in real_words if w.lower() in [
'update', 'download', 'install', 'error', 'success', 'failed',
'system', 'android', 'version', 'checking', 'package', 'verify'
] or w in ['IHDR', 'IDAT', 'IEND', 'sRGB', 'sBIT', 'pHYs', 'iTXt', 'tEXt', 'zTXt']]
print(f"\nKnown words found: {likely_real}")
# Check for PNG chunk names
png_chunks = ['IHDR', 'PLTE', 'IDAT', 'IEND', 'cHRM', 'gAMA', 'iCCP', 'sBIT', 'sRGB',
'bKGD', 'hIST', 'tRNS', 'pHYs', 'sPLT', 'tIME', 'iTXt', 'tEXt', 'zTXt']
for chunk in png_chunks:
cnt = text.count(chunk)
if cnt > 0:
print(f" PNG chunk '{chunk}': {cnt} times")

15
decode_png.py Normal file
View File

@@ -0,0 +1,15 @@
import sys
import io
# Set stdout to UTF-8
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"D:\Yuzu-GCA\screen.png", 'rb') as f:
data = f.read()
print(f"Total bytes: {len(data)}")
# Try UTF-16 LE decode
text = data.decode('utf-16-le', errors='replace')
print("=== Decoded as UTF-16 LE (first 10000 chars) ===")
print(text[:10000])

129
fix_png.py Normal file
View File

@@ -0,0 +1,129 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"D:\Yuzu-GCA\screen.png", 'rb') as f:
data = f.read()
# The file is UTF-16 LE encoded with some corruption.
# Let's fix the known issues:
# 1. Replace the corrupted first char (U+5852 from bytes 52 58)
# with correct U+0089 (89 00) + U+0050 (50 00)
# But wait - in UTF-16 LE, each char takes 2 bytes.
# We need to replace 2 bytes (52 58) with 4 bytes (89 00 50 00)
# Actually, let's approach differently:
# Convert the whole thing from UTF-16 LE to raw bytes,
# but fix the known corruptions.
# Step 1: parse as UTF-16 LE (skip BOM)
raw = data[2:]
# Each 2 bytes is one UTF-16 LE char
chars = []
for i in range(0, len(raw), 2):
if i + 1 < len(raw):
ch = raw[i] | (raw[i+1] << 8)
chars.append(ch)
print(f"Total chars: {len(chars)}")
# Step 2: Fix corruptions in the char array
# Char 0: U+5852 -> should be U+0089 (first PNG byte)
# But U+0089 is one char, and we also need U+0050 for 'P'
# So we need to insert a char
# The corruption pattern:
# - Char 0 (U+5852) replaces two chars: U+0089 and U+0050
# - Char 6 (U+000D) should be U+000A
# - Char 7 (U+000A) is extra, should be removed
# - Char 12 (U+000A) is extra, should be removed
# Let's verify by looking at what we expect:
# Expected chars for correct PNG:
# [0]=0x0089, [1]=0x0050('P'), [2]=0x004E('N'), [3]=0x0047('G'),
# [4]=0x000D, [5]=0x000A, [6]=0x001A, [7]=0x000A (signature end)
# [8]=0x0000, [9]=0x0000, [10]=0x0000, [11]=0x000D (IHDR length=13)
# [12]=0x0049('I'), [13]=0x0048('H'), [14]=0x0044('D'), [15]=0x0052('R')
# Actual chars:
# [0]=0x5852, [1]=0x004E, [2]=0x0047, [3]=0x000D, [4]=0x000A,
# [5]=0x001A, [6]=0x000D, [7]=0x000A, [8]=0x0000, [9]=0x0000,
# [10]=0x0000, [11]=0x000D, [12]=0x000A, [13]=0x0049, [14]=0x0048, ...
# Fix:
# Replace char 0 (U+5852) with U+0089
# Insert U+0050 at position 1
# Change char 6 from 0x0D to 0x0A
# Delete char 7 (extra 0x0A)
# Delete char 12 (extra 0x0A) - wait, this is after IHDR length
fixed_chars = []
# Fix char 0: split U+5852 -> U+0089, U+0050
fixed_chars.append(0x0089) # PNG first byte
fixed_chars.append(0x0050) # 'P'
# Copy chars 1-5 normally (but adjust indices)
# Original char 1 (U+004E='N') -> fixed index 2
# Original char 2 (U+0047='G') -> fixed index 3
# Original char 3 (U+000D) -> fixed index 4
# Original char 4 (U+000A) -> fixed index 5
# Original char 5 (U+001A) -> fixed index 6
fixed_chars.append(chars[1]) # 'N'
fixed_chars.append(chars[2]) # 'G'
fixed_chars.append(chars[3]) # '\r'
fixed_chars.append(chars[4]) # '\n'
fixed_chars.append(chars[5]) # '\x1a'
# Fix char 6: change from 0x0D to 0x0A
fixed_chars.append(0x000A) # Should be '\n' instead of '\r'
# Skip char 7 (extra 0x0A) - don't add it
# Add chars 8-11 (IHDR length = 0x0000000D)
fixed_chars.append(chars[8]) # 0x0000
fixed_chars.append(chars[9]) # 0x0000
fixed_chars.append(chars[10]) # 0x0000
fixed_chars.append(chars[11]) # 0x000D
# Skip char 12 (extra 0x0A) - don't add it
# Add the rest from char 13 onwards
for i in range(13, len(chars)):
fixed_chars.append(chars[i])
print(f"Fixed chars: {len(fixed_chars)}")
# Step 3: Convert fixed chars back to UTF-16 LE bytes
fixed_bytes = bytearray()
for ch in fixed_chars:
fixed_bytes.append(ch & 0xFF) # low byte
fixed_bytes.append((ch >> 8) & 0xFF) # high byte
print(f"Fixed bytes: {len(fixed_bytes)}")
# Step 4: Extract original PNG bytes (every other byte, starting from first)
# In UTF-16 LE, for ASCII chars (< 0x80), the low byte is the char and high byte is 00
# But for non-ASCII chars like U+0089, the low byte is 0x89 and high byte is 0x00
# So we take the low byte of each char
png_bytes = bytearray()
for ch in fixed_chars:
png_bytes.append(ch & 0xFF)
print(f"PNG bytes: {len(png_bytes)}")
print(f"First 32 PNG bytes: {' '.join(f'{b:02X}' for b in png_bytes[:32])}")
# Check PNG signature
if png_bytes[:8] == b'\x89PNG\r\n\x1a\n':
print("✓ Valid PNG signature!")
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'wb') as f:
f.write(png_bytes)
print("Saved screen_fixed.png")
else:
print(f"✗ Invalid signature: {' '.join(f'{b:02X}' for b in png_bytes[:8])}")
# Try to see if we can find where it goes wrong
expected = b'\x89PNG\r\n\x1a\n'
for i in range(8):
if png_bytes[i] != expected[i]:
print(f" Mismatch at byte {i}: got {png_bytes[i]:02X}, expected {expected[i]:02X}")

66
fix_png2.py Normal file
View File

@@ -0,0 +1,66 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'rb') as f:
data = f.read()
print(f"Original size: {len(data)}")
# The file has been corrupted by text-mode LF->CRLF conversion.
# Every 0x0A byte in the original PNG has been changed to 0x0D 0x0A.
# We need to revert this: change 0x0D 0x0A back to 0x0A.
# But the PNG signature contains a legitimate 0x0D 0x0A (at offset 4-5),
# which should remain as-is.
# Strategy: replace 0D 0A -> 0A, EXCEPT for the known legitimate ones
# Legitimate: offset 4-5 (PNG signature "\r\n")
# Actually, let's just do the replacement everywhere and then fix the signature
fixed = bytearray()
i = 0
while i < len(data):
if i < len(data) - 1 and data[i] == 0x0D and data[i+1] == 0x0A:
# CR LF found - replace with just LF
fixed.append(0x0A)
i += 2
else:
fixed.append(data[i])
i += 1
fixed = bytes(fixed)
print(f"After CRLF->LF fix: {len(fixed)}")
# Now fix the PNG signature (offset 4-5 should be 0D 0A, but we changed it to 0A)
# The correct PNG signature: 89 50 4E 47 0D 0A 1A 0A
# We need to fix offset 4 to be 0D again
if fixed[4] == 0x0A: # Should be 0x0D
fixed = fixed[:4] + b'\x0D' + fixed[5:]
print("Fixed PNG signature byte 4 (0D)")
print(f"First 16 bytes: {' '.join(f'{b:02X}' for b in fixed[:16])}")
# Check IHDR
offset = 8
length = int.from_bytes(fixed[offset:offset+4], 'big')
chunk_type = fixed[offset+4:offset+8].decode('ascii', errors='replace')
print(f"IHDR: type={chunk_type}, length={length}")
w = int.from_bytes(fixed[offset+8:offset+12], 'big')
h = int.from_bytes(fixed[offset+12:offset+16], 'big')
bit_depth = fixed[offset+16]
color_type = fixed[offset+17]
print(f" Width={w}, Height={h}, BitDepth={bit_depth}, ColorType={color_type}")
# Save
with open(r"D:\Yuzu-GCA\screen_fixed2.png", 'wb') as f:
f.write(fixed)
print("Saved screen_fixed2.png")
# Try to open with PIL
from PIL import Image
try:
img = Image.open(r"D:\Yuzu-GCA\screen_fixed2.png")
print(f"Success! Format={img.format}, Size={img.size}, Mode={img.mode}")
except Exception as e:
print(f"PIL error: {e}")

View File

@@ -1,3 +0,0 @@
// Root entry point for Expo dev server
// Redirect to client-android
import './packages/client-android/index';

48
ocr_fixed.py Normal file
View File

@@ -0,0 +1,48 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from PIL import Image
img = Image.open(r"D:\Yuzu-GCA\screen_fixed.png")
print(f"Format: {img.format}")
print(f"Size: {img.size}")
print(f"Mode: {img.mode}")
# Try OCR
try:
import pytesseract
text = pytesseract.image_to_string(img, lang='eng+chi_sim')
print("\n=== pytesseract OCR result ===")
print(text)
except ImportError:
print("pytesseract not available")
# Also try Windows OCR
try:
import asyncio
from winsdk.windows.media.ocr import OcrEngine
from winsdk.windows.graphics.imaging import BitmapDecoder, SoftwareBitmap
from winsdk.windows.storage.streams import RandomAccessStreamReference
from winsdk.windows.globalization import Language
import os
async def ocr_windows():
engine = OcrEngine.try_create_from_user_profile_languages()
if not engine:
engine = OcrEngine.try_create_from_language(Language("en"))
path = os.path.abspath(r"D:\Yuzu-GCA\screen_fixed.png")
file_stream = await RandomAccessStreamReference.create_from_file(path).open_read_async()
decoder = await BitmapDecoder.create_async(file_stream)
bitmap = await decoder.get_software_bitmap_async()
result = await engine.recognize_async(bitmap)
print("\n=== Windows OCR result ===")
for line in result.lines:
print(line.text)
asyncio.run(ocr_windows())
except Exception as e:
print(f"Windows OCR failed: {e}")

55
ocr_screen.py Normal file
View File

@@ -0,0 +1,55 @@
"""
OCR script to extract text from screen.png
Attempts to use pytesseract if available, otherwise falls back to basic image info.
"""
import sys
import os
image_path = r"D:\Yuzu-GCA\screen.png"
# First, try to get basic image info
try:
from PIL import Image
img = Image.open(image_path)
print(f"Image size: {img.size}")
print(f"Image mode: {img.mode}")
print(f"Image format: {img.format}")
print("---")
except ImportError:
print("Pillow not available, trying basic file read...")
# Can't do much without PIL
# Try pytesseract
try:
import pytesseract
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
print("OCR Result (pytesseract):")
print(text)
except ImportError:
print("pytesseract not available")
# Try subprocess call to system tesseract
import subprocess
try:
result = subprocess.run(
['tesseract', image_path, 'stdout', '-l', 'chi_sim+eng'],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
print("OCR Result (system tesseract):")
print(result.stdout)
else:
print(f"Tesseract error: {result.stderr}")
except FileNotFoundError:
print("System tesseract not found either")
except Exception as e:
print(f"Error running tesseract: {e}")
# If nothing works, try reading raw pixels to detect if it's a text screenshot
try:
from PIL import Image
img = Image.open(image_path)
# Print first few pixel values to understand the image
pixels = list(img.getdata())[:20]
print(f"\nFirst 20 pixel values: {pixels}")
except:
pass

23
ocr_screen2.py Normal file
View File

@@ -0,0 +1,23 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from PIL import Image
img = Image.open(r"D:\Yuzu-GCA\screen2.png")
print(f"Format: {img.format}, Size: {img.size}, Mode: {img.mode}")
# Try pytesseract OCR
try:
import pytesseract
# Configure tesseract path if needed
# pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
print("\n=== OCR Result ===")
print(text)
except ImportError:
print("\npytesseract not available")
except Exception as e:
print(f"\nOCR error: {e}")

43
ocr_script.ps1 Normal file
View File

@@ -0,0 +1,43 @@
# PowerShell script using Windows built-in OCR
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Runtime.WindowsRuntime
Add-Type -AssemblyName Windows.Foundation
Add-Type -AssemblyName Windows.Graphics.Imaging
Add-Type -AssemblyName Windows.Media.Ocr
$winRtTypes = [Windows.Media.Ocr.OcrEngine]::GetType()
# Load the image
$imagePath = "D:\Yuzu-GCA\screen2.png"
$bitmap = [System.Drawing.Bitmap]::FromFile($imagePath)
Write-Host "Image size: $($bitmap.Width) x $($bitmap.Height)"
# Convert to stream
$ms = New-Object System.IO.MemoryStream
$bitmap.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
$bitmap.Dispose()
# Create BitmapDecoder
$ms.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
$randomAccessStream = New-Object Windows.Storage.Streams.InMemoryRandomAccessStream
$outputStream = $randomAccessStream.GetOutputStreamAt(0)
$dataWriter = New-Object Windows.Storage.Streams.DataWriter($outputStream)
$bytes = $ms.ToArray()
$dataWriter.WriteBytes($bytes)
$dataWriter.StoreAsync().GetAwaiter().GetResult() | Out-Null
$dataWriter.FlushAsync().GetAwaiter().GetResult() | Out-Null
$ms.Dispose()
$decoder = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($randomAccessStream).GetAwaiter().GetResult()
$softwareBitmap = [Windows.Graphics.Imaging.SoftwareBitmap]::Convert($decoder.GetSoftwareBitmapAsync().GetAwaiter().GetResult(), [Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8, [Windows.Graphics.Imaging.BitmapAlphaMode]::Premultiplied)
# Perform OCR
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
$result = $engine.RecognizeAsync($softwareBitmap).GetAwaiter().GetResult()
# Output results
Write-Host "Recognized text:"
Write-Host "===================="
foreach ($line in $result.Lines) {
Write-Host $line.Text
}

51
ocr_script.py Normal file
View File

@@ -0,0 +1,51 @@
"""
OCR script to extract text from screen.png
"""
import sys
import os
image_path = r"D:\Yuzu-GCA\screen.png"
# Try pytesseract first
try:
import pytesseract
from PIL import Image
img = Image.open(image_path)
text = pytesseract.image_to_string(img, lang='eng+chi_sim')
print("=== pytesseract OCR result ===")
print(text)
sys.exit(0)
except ImportError:
print("pytesseract not available, trying alternative...")
# Try Windows OCR via winrt
try:
import asyncio
from winsdk.windows.media.ocr import OcrEngine
from winsdk.windows.graphics.imaging import BitmapDecoder
from winsdk.windows.storage.streams import RandomAccessStreamReference
from winsdk.windows.globalization import Language
async def ocr_windows():
# Get the default OCR engine for Chinese
engine = OcrEngine.try_create_from_user_profile_languages()
if not engine:
# Fallback to English
engine = OcrEngine.try_create_from_language(Language("en"))
# Open the image file
file_stream = await RandomAccessStreamReference.create_from_file(image_path).open_read_async()
decoder = await BitmapDecoder.create_async(file_stream)
bitmap = await decoder.get_software_bitmap_async()
result = await engine.recognize_async(bitmap)
print("=== Windows OCR result ===")
for line in result.lines:
print(line.text)
asyncio.run(ocr_windows())
sys.exit(0)
except ImportError:
print("winsdk not available either")
print("No OCR engine available. Please install pytesseract or Windows SDK.")

48
ocr_win.py Normal file
View File

@@ -0,0 +1,48 @@
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
print("Attempting Windows OCR...")
try:
import asyncio
from winsdk.windows.media.ocr import OcrEngine
from winsdk.windows.graphics.imaging import BitmapDecoder
from winsdk.windows.storage.streams import RandomAccessStreamReference
from winsdk.windows.globalization import Language
import os
async def ocr():
path = os.path.abspath(r"D:\Yuzu-GCA\screen2.png")
print(f"Opening: {path}")
# Try to get engine for Chinese
engine = OcrEngine.try_create_from_user_profile_languages()
if not engine:
print("No user profile languages, trying Chinese...")
engine = OcrEngine.try_create_from_language(Language("zh-Hans"))
if not engine:
print("Trying English...")
engine = OcrEngine.try_create_from_language(Language("en"))
if not engine:
print("No OCR engine available")
return
print(f"Using OCR engine: {engine.recognizer_language.display_name}")
file_stream = await RandomAccessStreamReference.create_from_file(path).open_read_async()
decoder = await BitmapDecoder.create_async(file_stream)
bitmap = await decoder.get_software_bitmap_async()
result = await engine.recognize_async(bitmap)
print(f"\n=== Windows OCR Result ({result.lines.size} lines) ===")
for line in result.lines:
print(line.text)
asyncio.run(ocr())
except ImportError as e:
print(f"winsdk not available: {e}")
except Exception as e:
print(f"Error: {e}")

View File

@@ -1,44 +1,18 @@
# OTA 热更新目录 # OTA 文件夹
## 文件说明 此目录存放 manifest 文件。manifest-stable.json 是 CI 构建时动态生成的产物,
也提交到仓库通过 raw URL 供客户端拉取。
| 文件 | 用途 | ## 文件
|------|------|
| `version.txt` | v5 嵌入式初始化——格式: `版本\|URL\|SHA256` |
| `h5-manifest-stable.json` | 发行渠道stable——格式: `{latest, min_version, url, sha256}` |
| `h5-manifest-beta.json` | 测试渠道beta |
| `h5-manifest-nightly.json` | 开发渠道nightly |
| `h5/index-vN.html` | 可下载的热更新页面 |
## 推送补丁 - `gen-manifest.sh` — 构建后运行,计算 APK SHA256 并生成 manifest-stable.json
- `manifest-stable.json` — stable 渠道
- `manifest-beta.json` — beta 渠道
- `manifest-nightly.json` — nightly 渠道
```bash ## 流程
# 更新所有渠道到 v7
cd ota
bash push-hotfix.sh 7 all
# 只更新 nightly 渠道
bash push-hotfix.sh 7 nightly
# 手动推送
git add ota/
git commit -m "hotfix: v7"
git push
``` ```
CI 构建 APK → ./ota/gen-manifest.sh → git push manifest-stable.json
## 格式 手机 App → raw URL 拉取 manifest → 比较版本 → 下载 APK → SHA256 校验
**h5-manifest-*.json:**
```json
{
"latest": "0.1.0-dav-android-6",
"min_version": "0.1.0-dav-android-5",
"url": "https://git.childish-ghost.com/.../ota/h5/index-v6.html",
"sha256": "33a16632..."
}
```
**version.txt:**
```
0.1.0-dav-android-6|https://.../ota/h5/index-v6.html|33a16632...
``` ```

12
ota/expo-manifest.json Normal file
View File

@@ -0,0 +1,12 @@
{
"id": "e5ce52a-0000-0000-0000-000000000000",
"createdAt": "2026-07-19T15:30:00.000Z",
"runtimeVersion": "0.1.0",
"launchAsset": {
"key": "bundle",
"contentType": "application/javascript",
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-8/bundle.js"
},
"assets": [],
"metadata": {}
}

View File

@@ -1,54 +1,38 @@
#!/bin/bash #!/bin/bash
# gen-manifest.sh — 生成 APK 发行版 manifest # OTA manifest 生成脚本
# 在 Android Studio 构建 Release APK 后运行 # 用法: ./ota/gen-manifest.sh <versionName> <versionCode> <apk-path>
# 用法: bash gen-manifest.sh 6 stable # 构建号规则: 每次生成APK时 versionCode +1, 对应 git tag vX.Y.Z-dav-android-N
# bash gen-manifest.sh 6 all # CI 构建时自动运行, SHA256动态填入, 生成 manifest-stable.json 后上传到 Gitea Release
set -e VERSION_NAME="${1:-0.1.0}"
VERSION_CODE="${2:-7}"
VER="$1" APK_PATH="${3:-packages/client-android/android/app/build/outputs/apk/debug/v0.1.0-dav-android-7-debug.apk}"
shift RELEASE_TAG="v${VERSION_NAME}-dav-android-${VERSION_CODE}"
CHANNELS=("$@") APK_NAME="v${VERSION_NAME}-dav-android-${VERSION_CODE}-debug.apk"
if [ -z "$VER" ] || [ ${#CHANNELS[@]} -eq 0 ]; then
echo "用法: $0 <构建号> <渠道...>"
echo "示例: $0 6 stable beta nightly"
exit 1
fi
OTA_DIR="$(dirname "$0")"
APK_PATH="$OTA_DIR/../packages/client-android/android/app/build/outputs/apk/debug/v0.1.0-dav-android-${VER}-debug.apk"
BASE="https://git.childish-ghost.com/LukeMackin/Yuzu-GCA"
TAG="v0.1.0-dav-android-${VER}"
APK_NAME="v0.1.0-dav-android-${VER}-debug.apk"
DL_URL="${BASE}/releases/download/${TAG}/${APK_NAME}"
if [ ! -f "$APK_PATH" ]; then
echo "APK not found: $APK_PATH"
exit 1
fi
SHA256=$(sha256sum "$APK_PATH" | cut -d' ' -f1) SHA256=$(sha256sum "$APK_PATH" | cut -d' ' -f1)
echo "APK : $APK_PATH" SIZE=$(stat -c%s "$APK_PATH")
echo "SHA256: $SHA256"
for CH in "${CHANNELS[@]}"; do cat > ota/manifest-stable.json <<JSON
if [ "$CH" = "all" ]; then {
for CH in stable beta nightly; do "version": "${VERSION_NAME}",
cat > "$OTA_DIR/manifest-${CH}.json" <<EOF "builds": {
{"version":"0.1.0-dav-android-${VER}","versionCode":${VER},"url":"${DL_URL}","sha256":"${SHA256}"} "android": {
EOF "type": "apk",
echo " ✓ manifest-${CH}.json" "tag": "${RELEASE_TAG}",
done "url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/${RELEASE_TAG}/${APK_NAME}",
break "sha256": "${SHA256}",
fi "size": ${SIZE},
cat > "$OTA_DIR/manifest-${CH}.json" <<EOF "channel": "stable",
{"version":"0.1.0-dav-android-${VER}","versionCode":${VER},"url":"${DL_URL}","sha256":"${SHA256}"} "minVersion": "${VERSION_NAME}"
EOF }
echo " ✓ manifest-${CH}.json" }
done }
JSON
echo "Generated: ota/manifest-stable.json"
echo " tag: ${RELEASE_TAG}"
echo " sha256: ${SHA256}"
echo " size: ${SIZE}"
echo "" echo ""
echo "Next: git tag $TAG && git push --tags" echo "Upload: curl -X POST 'https://git.childish-ghost.com/api/v1/repos/LukeMackin/Yuzu-GCA/releases/\${RELEASE_ID}/assets?name=manifest-stable.json' -H 'Authorization: token \$TOKEN' -H 'Content-Type: application/json' --data-binary @ota/manifest-stable.json"
echo " Gitea Release → upload APK"
echo " bash push-hotfix.sh $VER stable # H5同步"

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=index-stable.html"></head><body></body></html>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=index-stable.html"></head><body></body></html>

View File

@@ -1,80 +0,0 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui;background:#0f172a;color:#e2e8f0;padding:8px 16px 16px;margin:0;overflow-x:hidden}
h1{font-size:18px;color:#38bdf8}
.card{background:#1e293b;border-radius:8px;padding:16px;margin:12px 0}
.card-title{font-size:14px;color:#94a3b8;margin-bottom:8px}
.btn{display:block;width:100%;padding:14px;margin:8px 0;border:none;border-radius:8px;font-size:16px;cursor:pointer}
.btn-check{background:#1e40af;color:#fff}
.btn-download{background:#15803d;color:#fff;display:none}
.btn-download.show{display:block}
.log{background:#0f172a;padding:12px;border-radius:8px;margin-top:12px;max-height:200px;overflow-y:auto;font-size:11px;white-space:pre-wrap;color:#94a3b8}
.ver{color:#94a3b8;font-size:12px;margin:4px 0}
.gear{position:absolute;top:20px;right:16px;font-size:20px;cursor:pointer;color:#94a3b8}
.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.7);z-index:99}
.modal.show{display:flex;align-items:center;justify-content:center}
.modal-box{background:#1e293b;border-radius:12px;padding:24px;width:80%;max-width:360px}
.modal-title{font-size:16px;color:#38bdf8;margin-bottom:16px}
.ch-opt{display:block;width:100%;padding:12px;margin:6px 0;border:1px solid #334155;border-radius:6px;background:transparent;color:#e2e8f0;font-size:14px;cursor:pointer;text-align:left}
.ch-opt.on{border-color:#38bdf8;background:#1e40af40}
.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;margin-left:8px}
.badge-new{background:#dc2626;color:#fff}
</style></head><body>
<div class="gear" onclick="openSettings()">&#9881;</div>
<h1>Yuzu GCA</h1>
<div class="ver"><span id="ver"></span><span id="badge" class="badge badge-new" style="display:none">NEW</span></div>
<div class="card"><div class="card-title">Channel: <span id="chLabel">Stable</span></div></div>
<button class="btn btn-check" onclick="checkUpdate()">Check Update</button>
<button class="btn btn-download" id="btnApply" onclick="applyUpdate()">Download Update</button>
<div class="log" id="log"></div>
<div class="modal" id="modal">
<div class="modal-box">
<div class="modal-title">Select Channel</div>
<button class="ch-opt on" onclick="setChannel('stable')">Stable</button>
<button class="ch-opt" onclick="setChannel('beta')">Beta</button>
<button class="ch-opt" onclick="setChannel('nightly')">Nightly</button>
<button class="btn btn-check" style="margin-top:12px" onclick="closeSettings()">Close</button>
</div>
</div>
<script>
var channel=localStorage.getItem('gca_channel')||'stable';
document.getElementById('chLabel').innerText=(localStorage.getItem('gca_channel')||'stable').charAt(0).toUpperCase()+(localStorage.getItem('gca_channel')||'stable').slice(1);
var dlUrl='',dlVer='',dlSha='';
function log(m){var l=document.getElementById('log');l.innerText+=m+'\n';l.scrollTop=l.scrollHeight}
function openSettings(){
var m=document.getElementById('modal');m.classList.add('show');
document.querySelectorAll('.ch-opt').forEach(function(b){b.classList.toggle('on',b.textContent.toLowerCase()===channel)})
}
function closeSettings(){document.getElementById('modal').classList.remove('show')}
function setChannel(ch){
channel=ch;localStorage.setItem('gca_channel',ch);
document.getElementById('chLabel').innerText=ch.charAt(0).toUpperCase()+ch.slice(1);
document.querySelectorAll('.ch-opt').forEach(function(b){b.classList.toggle('on',b.textContent.toLowerCase()===ch)});
closeSettings();checkUpdate()
}
function checkUpdate(){
log('channel:'+channel);
var j=JSON.parse(Android.fetchManifest(channel));
if(j.error){log('ERR:'+j.error);return}
var h5=j.h5;log('latest:'+h5.latest+' min:'+h5.min_version);
var v=(h5.min_version||'').replace(/[^0-9]/g,''),my=(document.getElementById('ver').innerText||'').replace(/[^0-9]/g,'');
if(parseInt(my||'0')<parseInt(v||'0')){log('ERR:APK too old (need v'+h5.min_version+')');return}
dlUrl=h5.url;dlSha=h5.sha256||'';
if(!dlSha||/^(0{64}|TBD)/.test(dlSha)){log('ERR:channel no build');return}
if(h5.latest!==document.getElementById('ver').innerText&&h5.latest!=='v'+document.getElementById('ver').innerText){
dlVer=h5.latest;document.getElementById('btnApply').classList.add('show');log('***NEW VERSION***')}
else log('up to date')
}
function applyUpdate(){
log('download...');
var t=Android.fetchText('h5/index-stable.html');
if(typeof t!=='string'||t.startsWith('ERROR:')){log('ERR:'+t);return}
var h=Android.sha256(t);
if(h!==dlSha){log('SHA256 MISMATCH!');return}
Android.saveCache(t);log('OK! restart app')
}
document.getElementById('ver').innerText=Android.fetchManifest(channel).match(/"latest":"([^"]+)"/)?.[1]||channel;
</script></body></html>

1
ota/manifest-beta.json Normal file
View File

@@ -0,0 +1 @@
{"version":"0.1.0","builds":{}}

View File

@@ -0,0 +1 @@
{"version":"0.1.0","builds":{"android":{"type":"apk","tag":"v0.1.0-dav-android-8","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-8/v0.1.0-dav-android-8-debug.apk","sha256":"b2451a398ea2dd34e585c5ec4529d6654451c9b9ab4ddf4a25ec9b032f0ac2bf","size":3506569,"channel":"nightly","minVersion":"0.1.0"}}}

1
ota/manifest-stable.json Normal file
View File

@@ -0,0 +1 @@
{"version":"0.1.0","builds":{}}

View File

@@ -1 +0,0 @@
{"stable":{"h5":{"latest":"0.1.0-dav-android-6","min_version":"0.1.0-dav-android-5","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/h5/index-stable.html","sha256":"ae7db3b84ad018fbc4cc938c30db482dd4c3cad47aab6db81c8a6716cfadc844"},"apk":{"version":"0.1.0-dav-android-5","versionCode":5,"url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-5/v0.1.0-dav-android-5-debug.apk","sha256":"56b0e384f53015b0a1a5eb2507fd792dfc31fe44aff40342cd8a8d3bd5c1e14a"}},"beta":{"h5":{"latest":"0.1.0-dav-android-6","min_version":"0.1.0-dav-android-5","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/h5/index-stable.html","sha256":"ae7db3b84ad018fbc4cc938c30db482dd4c3cad47aab6db81c8a6716cfadc844"},"apk":{"version":"0.1.0-dav-android-5","versionCode":5,"url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-5/v0.1.0-dav-android-5-debug.apk","sha256":"56b0e384f53015b0a1a5eb2507fd792dfc31fe44aff40342cd8a8d3bd5c1e14a"}},"nightly":{"h5":{"latest":"0.1.0-dav-android-7","min_version":"0.1.0-dav-android-5","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/h5/index-stable.html","sha256":"ae7db3b84ad018fbc4cc938c30db482dd4c3cad47aab6db81c8a6716cfadc844"},"apk":{"version":"0.1.0-dav-android-5","versionCode":5,"url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-5/v0.1.0-dav-android-5-debug.apk","sha256":"56b0e384f53015b0a1a5eb2507fd792dfc31fe44aff40342cd8a8d3bd5c1e14a"}}}

View File

@@ -1,79 +0,0 @@
#!/bin/bash
# push-hotfix.sh — 推送 H5 热更新补丁到 Gitea
#
# 用法:
# ./push-hotfix.sh 7 stable # 只更新 stable 渠道
# ./push-hotfix.sh 7 all # 更新全部三个渠道
# ./push-hotfix.sh 7 beta nightly # 更新指定渠道
#
# 流程:
# 1. 创建 ota/h5/index-v{VER}.html如果不存在则从模板生成
# 2. 计算 SHA256
# 3. 更新 ota/h5-manifest-{CHANNEL}.json
# 4. 如果是稳定版,更新 ota/version.txt
# 5. git add + commit + push
set -e
VER="$1"
shift
CHANNELS=("$@")
if [ -z "$VER" ] || [ ${#CHANNELS[@]} -eq 0 ]; then
echo "用法: $0 <版本号> <渠道...>"
echo "示例: $0 7 stable beta nightly"
exit 1
fi
OTA_DIR="$(dirname "$0")"
H5_DIR="$OTA_DIR/h5"
BASE_URL="https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota"
# 1. 创建 HTML 文件(如果不存在)
HTML_FILE="$H5_DIR/index-v${VER}.html"
if [ ! -f "$HTML_FILE" ]; then
echo "→ 请手动创建 $HTML_FILE 或从 index-v6.html 复制修改"
exit 1
fi
# 2. 计算 SHA256
SHA256=$(sha256sum "$HTML_FILE" | cut -d' ' -f1)
echo "SHA256: $SHA256"
# 3. 更新 manifest
FULL_URL="${BASE_URL}/h5/index-v${VER}.html"
for CH in "${CHANNELS[@]}"; do
if [ "$CH" = "all" ]; then
for CH in stable beta nightly; do
MANIFEST="$OTA_DIR/h5-manifest-${CH}.json"
cat > "$MANIFEST" <<EOF
{"latest":"0.1.0-dav-android-${VER}","min_version":"0.1.0-dav-android-5","url":"${FULL_URL}","sha256":"${SHA256}"}
EOF
echo " ✓ h5-manifest-${CH}.json"
done
break
fi
MANIFEST="$OTA_DIR/h5-manifest-${CH}.json"
cat > "$MANIFEST" <<EOF
{"latest":"0.1.0-dav-android-${VER}","min_version":"0.1.0-dav-android-5","url":"${FULL_URL}","sha256":"${SHA256}"}
EOF
echo " ✓ h5-manifest-${CH}.json"
done
# 4. 稳定版更新 version.txt
if [[ " ${CHANNELS[*]} " =~ " stable " ]] || [[ " ${CHANNELS[*]} " =~ " all " ]]; then
echo "0.1.0-dav-android-${VER}|${FULL_URL}|${SHA256}" > "$OTA_DIR/version.txt"
echo " ✓ version.txt"
fi
# 5. git push
echo ""
echo "commit & push?"
read -p "(y/N) " yn
if [ "$yn" = "y" ] || [ "$yn" = "Y" ]; then
git add "$OTA_DIR"
git commit -m "hotfix: v${VER}${CHANNELS[*]}"
git push
echo "✓ pushed"
fi

View File

@@ -1 +0,0 @@
https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/manifest.json

View File

@@ -1,286 +1,72 @@
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { import { StyleSheet, Text, View, ActivityIndicator } from 'react-native';
StyleSheet, Text, View, TextInput, TouchableOpacity, import { onAppStartCheckUpdate } from './src/updater/index';
ScrollView, Switch, FlatList, KeyboardAvoidingView, Platform,
} from 'react-native';
import { DEFAULT_GATEWAY_CONFIG } from '@yuzu-gca/shared';
/** Android Client: Chat + Settings + Connection */
type Msg = { role: 'user' | 'assistant'; text: string; ts: number };
export default function App() { export default function App() {
const [tab, setTab] = useState<'chat' | 'terminal' | 'settings' | 'device' | 'connection'>('chat'); const [status, setStatus] = useState<'checking' | 'up-to-date' | 'updating' | 'error'>('checking');
const [message, setMessage] = useState('正在检查更新...');
// Chat state useEffect(() => {
const [messages, setMessages] = useState<Msg[]>([]); (async () => {
const [input, setInput] = useState('');
const [sending, setSending] = useState(false);
// Settings state
const [gatewayUrl, setGatewayUrl] = useState(DEFAULT_GATEWAY_CONFIG.url);
const [deviceName, setDeviceName] = useState('Android Device');
const [autoConnect, setAutoConnect] = useState(true);
const [heartbeatInterval, setHeartbeatInterval] = useState(String(DEFAULT_GATEWAY_CONFIG.heartbeatInterval / 1000));
const [logLevel, setLogLevel] = useState('info');
// Connection state
const [connStatus, setConnStatus] = useState<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected');
// Terminal state
const [termCmd, setTermCmd] = useState('');
const [termOut, setTermOut] = useState('Execute remote commands via Gateway MCP.\nType a shell command and press Execute.\n');
const [termRunning, setTermRunning] = useState(false);
const runTermCmd = async () => {
if (!termCmd.trim() || termRunning) return;
const c = termCmd.trim(); setTermCmd(''); setTermRunning(true);
setTermOut(o => o + `$ ${c}\n`);
try { try {
const r = await fetch(`${gatewayUrl}/tools/exec`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({command:c}), signal:AbortSignal.timeout(30000) }); const result = await onAppStartCheckUpdate();
if (!r.ok) throw new Error(`HTTP ${r.status}`); if (result.jsUpdate) {
const d = await r.json() as any; setStatus('updating');
setTermOut(o => o + (d.stdout||d.stderr||JSON.stringify(d)) + '\n'); setMessage('发现 JS 更新,正在下载...(下次启动生效)');
} catch(e:any) { setTermOut(o => o + `Error: ${e.message}\n`); } } else if (result.apkUpdate) {
finally { setTermRunning(false); } setStatus('updating');
}; setMessage('安装包已就绪,重启后生效');
} else {
/** Send chat message to Gateway */ setStatus('up-to-date');
const sendMessage = async () => { setMessage('已是最新版本');
const text = input.trim();
if (!text || sending) return;
const userMsg: Msg = { role: 'user', text, ts: Date.now() };
setMessages(prev => [...prev, userMsg]);
setInput('');
setSending(true);
try {
const resp = await fetch(`${gatewayUrl}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text }),
signal: AbortSignal.timeout(30000),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json() as { reply?: string; error?: string };
const aiMsg: Msg = {
role: 'assistant',
text: data.reply ?? data.error ?? 'No response',
ts: Date.now(),
};
setMessages(prev => [...prev, aiMsg]);
} catch (e: any) {
setMessages(prev => [...prev, {
role: 'assistant',
text: `Error: ${e.message}`,
ts: Date.now(),
}]);
} finally {
setSending(false);
} }
}; } catch (e: any) {
setStatus('error');
/** Connect/disconnect toggle */ setMessage(`更新检查失败: ${e.message}`);
const toggleConnection = () => { }
setConnStatus(prev => { })();
if (prev === 'disconnected' || prev === 'error') return 'connecting'; }, []);
return 'disconnected';
});
};
// ── Render ──────────────────────────────────────────────
return ( return (
<KeyboardAvoidingView style={styles.root} behavior={Platform.OS === 'ios' ? 'padding' : undefined}> <View style={styles.container}>
<StatusBar style="light" /> <StatusBar style="light" />
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Yuzu GCA</Text> <Text style={styles.title}>Yuzu GCA</Text>
<View style={[styles.statusDot, styles[`dot_${connStatus}` as keyof typeof styles]]} /> <Text style={styles.subtitle}>Global Control Assistant</Text>
</View> <View style={styles.statusBar}>
{status === 'checking' && <ActivityIndicator color="#3b82f6" />}
{/* Tabs */} <Text style={styles.statusText}>{message}</Text>
<View style={styles.tabBar}>
{(['chat', 'terminal', 'settings', 'device', 'connection'] as const).map(t => (
<TouchableOpacity key={t} onPress={() => setTab(t)} style={[styles.tab, tab === t && styles.tabActive]}>
<Text style={[styles.tabText, tab === t && styles.tabTextActive]}>
{t === 'chat' ? 'Chat' : t === 'terminal' ? 'Term' : t === 'settings' ? 'Gateway' : t === 'device' ? 'Device' : (connStatus === 'connected' ? '🟢' : '⚫')}
</Text>
</TouchableOpacity>
))}
</View>
{/* ── Chat Tab ── */}
{tab === 'chat' && (
<View style={styles.chatContainer}>
<FlatList
data={messages}
keyExtractor={(_, i) => String(i)}
style={styles.chatList}
renderItem={({ item }) => (
<View style={[styles.bubble, item.role === 'user' ? styles.bubbleUser : styles.bubbleAI]}>
<Text style={styles.bubbleRole}>{item.role === 'user' ? 'You' : 'GCA'}</Text>
<Text style={styles.bubbleText}>{item.text}</Text>
</View>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>Send a message to control your devices via AI</Text>
}
/>
<View style={styles.inputBar}>
<TextInput
style={styles.chatInput}
value={input}
onChangeText={setInput}
placeholder="Type a command..."
placeholderTextColor="#475569"
onSubmitEditing={sendMessage}
returnKeyType="send"
/>
<TouchableOpacity style={[styles.sendBtn, sending && styles.sendBtnDisabled]} onPress={sendMessage} disabled={sending}>
<Text style={styles.sendBtnText}>{sending ? '...' : 'Send'}</Text>
</TouchableOpacity>
</View> </View>
</View> </View>
)}
{/* ── Terminal Tab ── */}
{tab === 'terminal' && (
<View style={styles.chatContainer}>
<ScrollView style={styles.termOutput}>
<Text style={styles.termText} selectable>{termOut}</Text>
</ScrollView>
<View style={styles.inputBar}>
<TextInput style={styles.chatInput} value={termCmd} onChangeText={setTermCmd} placeholder="ls -la /home" placeholderTextColor="#475569" autoCapitalize="none" autoCorrect={false} onSubmitEditing={runTermCmd} returnKeyType="send" />
<TouchableOpacity style={[styles.sendBtn, termRunning && styles.sendBtnDisabled]} onPress={runTermCmd} disabled={termRunning}>
<Text style={styles.sendBtnText}>{termRunning ? '...' : 'Run'}</Text>
</TouchableOpacity>
</View>
</View>
)}
{/* ── Settings Tab ── */}
{tab === 'settings' && (
<ScrollView style={styles.body}>
<Text style={styles.label}>Gateway URL</Text>
<TextInput style={styles.input} value={gatewayUrl} onChangeText={setGatewayUrl} placeholder="http://localhost:3000" placeholderTextColor="#475569" autoCapitalize="none" keyboardType="url" />
<Text style={styles.label}>Heartbeat (seconds)</Text>
<TextInput style={styles.input} value={heartbeatInterval} onChangeText={setHeartbeatInterval} keyboardType="numeric" placeholder="15" placeholderTextColor="#475569" />
<View style={styles.switchRow}>
<Text style={styles.label}>Auto-connect on start</Text>
<Switch value={autoConnect} onValueChange={setAutoConnect} trackColor={{ true: '#3b82f6' }} />
</View>
<Text style={styles.label}>Log Level</Text>
<View style={styles.chipRow}>
{(['debug', 'info', 'warn', 'error'] as const).map(lv => (
<TouchableOpacity key={lv} style={[styles.chip, logLevel === lv && styles.chipActive]} onPress={() => setLogLevel(lv)}>
<Text style={[styles.chipText, logLevel === lv && styles.chipTextActive]}>{lv}</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
)}
{/* ── Device Tab ── */}
{tab === 'device' && (
<ScrollView style={styles.body}>
<Text style={styles.label}>Device Name</Text>
<TextInput style={styles.input} value={deviceName} onChangeText={setDeviceName} placeholder="Android Device" placeholderTextColor="#475569" />
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>Device Info</Text>
<Text style={styles.infoText}>Platform: Android</Text>
<Text style={styles.infoText}>Gateway: {gatewayUrl}</Text>
<Text style={styles.infoText}>Capabilities: exec, file_list, sysinfo...</Text>
<Text style={styles.infoText}>MCP Tools: 36 defined, 7 implemented</Text>
</View>
</ScrollView>
)}
{/* ── Connection Tab ── */}
{tab === 'connection' && (
<View style={styles.body}>
<View style={[styles.connIndicator, styles[`conn_${connStatus}` as keyof typeof styles]]}>
<Text style={styles.connText}>
{connStatus === 'connected' ? '🟢 Connected' :
connStatus === 'connecting' ? '🟡 Connecting...' :
connStatus === 'error' ? '🔴 Error' : '⚫ Disconnected'}
</Text>
</View>
<TouchableOpacity
style={[styles.btn, connStatus === 'connected' ? styles.btnDanger : styles.btnPrimary]}
onPress={toggleConnection}
>
<Text style={styles.btnText}>{connStatus === 'disconnected' ? 'Connect' : 'Disconnect'}</Text>
</TouchableOpacity>
</View>
)}
</KeyboardAvoidingView>
); );
} }
// ── Styles ────────────────────────────────────────────────
const styles = StyleSheet.create({ const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: '#0f172a' }, container: {
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 48, paddingBottom: 8 }, flex: 1,
title: { fontSize: 22, fontWeight: '800', color: '#e2e8f0' }, backgroundColor: '#0f172a',
statusDot: { width: 10, height: 10, borderRadius: 5 }, alignItems: 'center',
dot_connected: { backgroundColor: '#22c55e' }, justifyContent: 'center',
dot_connecting: { backgroundColor: '#eab308' }, padding: 24,
dot_error: { backgroundColor: '#ef4444' }, },
dot_disconnected: { backgroundColor: '#475569' }, title: {
fontSize: 28,
// Tabs fontWeight: '800',
tabBar: { flexDirection: 'row', marginHorizontal: 16, marginBottom: 8 }, color: '#e2e8f0',
tab: { flex: 1, padding: 10, alignItems: 'center', borderBottomWidth: 2, borderBottomColor: '#1e293b' }, marginBottom: 4,
tabActive: { borderBottomColor: '#3b82f6' }, },
tabText: { fontSize: 13, color: '#64748b' }, subtitle: {
tabTextActive: { color: '#3b82f6', fontWeight: '600' }, fontSize: 14,
color: '#94a3b8',
// Chat marginBottom: 32,
chatContainer: { flex: 1 }, },
chatList: { flex: 1, paddingHorizontal: 12 }, statusBar: {
bubble: { marginVertical: 4, padding: 10, borderRadius: 10, maxWidth: '85%' }, flexDirection: 'row',
bubbleUser: { alignSelf: 'flex-end', backgroundColor: '#1e40af' }, alignItems: 'center',
bubbleAI: { alignSelf: 'flex-start', backgroundColor: '#1e293b', borderWidth: 1, borderColor: '#334155' }, gap: 8,
bubbleRole: { fontSize: 10, color: '#94a3b8', marginBottom: 2 }, },
bubbleText: { fontSize: 14, color: '#e2e8f0' }, statusText: {
emptyText: { textAlign: 'center', color: '#475569', marginTop: 40, fontSize: 14 }, fontSize: 14,
inputBar: { flexDirection: 'row', padding: 8, paddingBottom: 16, gap: 8 }, color: '#94a3b8',
chatInput: { flex: 1, backgroundColor: '#1e293b', color: '#e2e8f0', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14 }, },
sendBtn: { backgroundColor: '#1e40af', borderRadius: 8, paddingHorizontal: 16, justifyContent: 'center' },
sendBtnDisabled: { opacity: 0.5 },
sendBtnText: { color: '#fff', fontWeight: '600', fontSize: 14 },
// Settings
body: { flex: 1, paddingHorizontal: 16 },
label: { fontSize: 12, color: '#94a3b8', marginBottom: 4, marginTop: 12 },
input: { backgroundColor: '#1e293b', color: '#e2e8f0', borderRadius: 8, padding: 12, fontSize: 14 },
switchRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 12 },
chipRow: { flexDirection: 'row', gap: 8, marginTop: 4 },
chip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: '#1e293b' },
chipActive: { backgroundColor: '#1e40af' },
chipText: { fontSize: 12, color: '#94a3b8' },
chipTextActive: { color: '#fff' },
infoCard: { backgroundColor: '#1e293b', borderRadius: 8, padding: 16, marginTop: 16 },
infoTitle: { fontSize: 14, color: '#38bdf8', marginBottom: 8 },
infoText: { fontSize: 12, color: '#94a3b8', marginTop: 4 },
// Connection
connIndicator: { borderRadius: 8, padding: 16, marginTop: 16, alignItems: 'center' },
conn_connected: { backgroundColor: '#064e3b' },
conn_connecting: { backgroundColor: '#78350f' },
conn_error: { backgroundColor: '#7f1d1d' },
conn_disconnected: { backgroundColor: '#1e293b' },
connText: { fontSize: 16, color: '#e2e8f0' },
termOutput: { flex: 1, backgroundColor: '#0a0f1a', margin: 8, borderRadius: 8, padding: 12 },
termText: { fontFamily: 'monospace', fontSize: 12, color: '#22c55e', lineHeight: 18 },
btn: { borderRadius: 8, padding: 14, marginTop: 16, alignItems: 'center' },
btnPrimary: { backgroundColor: '#1e40af' },
btnDanger: { backgroundColor: '#991b1b' },
btnText: { fontSize: 16, color: '#fff', fontWeight: '600' },
}); });

View File

@@ -1,16 +0,0 @@
# OSX
#
.DS_Store
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
# Bundle artifacts
*.jsbundle

View File

@@ -1,2 +1,2 @@
#Wed Jul 22 19:01:51 CST 2026 #Sun Jul 19 10:50:39 CST 2026
gradle.version=8.13 gradle.version=8.10.2

View File

@@ -9,7 +9,7 @@ android {
applicationId "dev.yuzu.gca" applicationId "dev.yuzu.gca"
minSdk 24 minSdk 24
targetSdk 35 targetSdk 35
versionCode 5 versionCode 8
versionName "0.1.0" versionName "0.1.0"
} }

View File

@@ -1,14 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:

View File

@@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -1,29 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/> <application
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> android:label="Yuzu GCA v0.1.0"
<queries> android:allowBackup="false"
<intent> android:supportsRtl="true"
<action android:name="android.intent.action.VIEW"/> android:theme="@style/Theme.AppCompat.DayNight">
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/> <activity
</intent> android:name=".MainActivity"
</queries> android:label="Yuzu GCA v0.1.0"
<application android:usesCleartextTraffic="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:supportsRtl="true"> android:exported="true">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/><activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="dev.yuzu.gca"/>
</intent-filter>
</activity> </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> </application>
</manifest> </manifest>

View File

@@ -1,84 +0,0 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,user-scalable=no">
<title>Yuzu GCA</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui;background:#0f172a;color:#e2e8f0;padding:16px}
h1{font-size:20px;color:#38bdf8;margin-bottom:4px}
.info{color:#94a3b8;font-size:12px;margin-bottom:16px}
.card{background:#1e293b;border-radius:8px;padding:16px;margin:12px 0}
.card-title{font-size:13px;color:#94a3b8;margin-bottom:8px}
.btn{display:block;width:100%;padding:14px;margin:8px 0;border:none;border-radius:8px;font-size:15px;cursor:pointer;color:#fff}
.btn-blue{background:#1e40af}
.btn-green{background:#15803d}
.input{display:block;width:100%;padding:12px;margin:8px 0;border:none;border-radius:8px;font-size:14px;background:#0f172a;color:#e2e8f0}
.log{background:#0a0f1a;padding:12px;border-radius:8px;margin-top:8px;max-height:250px;overflow-y:auto;font-size:11px;white-space:pre-wrap;color:#22c55e;font-family:monospace}
.tab{display:inline-block;padding:8px 16px;margin-right:4px;border-radius:6px 6px 0 0;background:#1e293b;color:#64748b;font-size:13px;cursor:pointer}
.tab-on{background:#1e40af;color:#fff}
</style></head><body>
<h1>Yuzu GCA</h1>
<div class="info">Self-contained — no Gateway needed</div>
<div>
<span class="tab tab-on" onclick="switchTab('exec',event)">Exec</span>
<span class="tab" onclick="switchTab('files',event)">Files</span>
<span class="tab" onclick="switchTab('sysinfo',event)">SysInfo</span>
</div>
<div class="card" id="tab-exec">
<div class="card-title">Shell Command (local)</div>
<input class="input" id="cmd" placeholder="e.g. ls, whoami, df -h">
<button class="btn btn-blue" onclick="runExec()">Execute</button>
<div class="log" id="log-exec"></div>
</div>
<div class="card" id="tab-files" style="display:none">
<div class="card-title">List Files (local)</div>
<input class="input" id="path" placeholder="/sdcard or ." value=".">
<button class="btn btn-blue" onclick="runFiles()">List</button>
<div class="log" id="log-files"></div>
</div>
<div class="card" id="tab-sysinfo" style="display:none">
<div class="card-title">Device Info (local)</div>
<button class="btn btn-blue" onclick="runSysinfo()">Refresh</button>
<div class="log" id="log-sysinfo"></div>
</div>
<script>
function switchTab(t,ev){
ev.target.classList.add('tab-on');
['exec','files','sysinfo'].filter(function(x){return x!==t}).forEach(function(x){
document.getElementById('tab-'+x).style.display='none';
document.querySelectorAll('.tab').forEach(function(b){if(b.textContent.toLowerCase().indexOf(x)>=0)b.classList.remove('tab-on')});
});
document.getElementById('tab-'+t).style.display='block';
}
function log(id,msg){
var l=document.getElementById(id);
l.innerText+=msg+'\n';l.scrollTop=l.scrollHeight;
}
function runExec(){
var c=document.getElementById('cmd').value, id='log-exec';
document.getElementById(id).innerText='';
log(id,'$ '+c);
var r=Android.exec(c);
log(id,r||'(empty)');
}
function runFiles(){
var p=document.getElementById('path').value, id='log-files';
document.getElementById(id).innerText='';
log(id,'ls '+p);
var r=Android.listFiles(p);
log(id,r||'(empty)');
}
function runSysinfo(){
var id='log-sysinfo';
document.getElementById(id).innerText='';
log(id,Android.sysinfo());
}
</script></body></html>

View File

@@ -1,58 +1,255 @@
package dev.yuzu.gca package dev.yuzu.gca
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageInstaller
import android.graphics.Color
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.provider.Settings
import android.webkit.WebView import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.io.BufferedReader import java.io.FileOutputStream
import java.io.InputStreamReader import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import android.util.Log
import androidx.core.content.FileProvider
import org.json.JSONObject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var logView: TextView
private var selectedChannel = "stable"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WebView(this).apply {
settings.javaScriptEnabled = true val scroll = ScrollView(this)
settings.domStorageEnabled = true val layout = LinearLayout(this).apply {
settings.allowUniversalAccessFromFileURLs = true orientation = LinearLayout.VERTICAL
addJavascriptInterface(LocalBridge(), "Android") setPadding(24, 24, 24, 24)
loadUrl("file:///android_asset/index.html")
}.also { setContentView(it) }
} }
inner class LocalBridge { val title = TextView(this).apply {
@android.webkit.JavascriptInterface text = "Yuzu GCA"
fun exec(cmd: String): String { textSize = 20f
return try { setPadding(0, 0, 0, 4)
val p = Runtime.getRuntime().exec(arrayOf("sh", "-c", cmd)) }
val out = BufferedReader(InputStreamReader(p.inputStream)).readText() layout.addView(title)
val err = BufferedReader(InputStreamReader(p.errorStream)).readText()
p.waitFor() val versionInfo = TextView(this).apply {
if (out.isNotEmpty()) out else err text = "v0.1.0-dav-android-8-debug"
} catch (e: Exception) { "ERR: ${e.message}" } textSize = 12f
setTextColor(Color.GRAY)
setPadding(0, 0, 0, 12)
}
layout.addView(versionInfo)
val channelLabel = TextView(this).apply {
text = "选择更新渠道:"
textSize = 13f
setPadding(0, 8, 0, 4)
}
layout.addView(channelLabel)
// 渠道按钮行
val channelRow = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(0, 0, 0, 12)
} }
@android.webkit.JavascriptInterface fun makeChannelBtn(label: String, channel: String): Button {
fun listFiles(path: String): String { return Button(this).apply {
return try { text = label
val dir = File(path) textSize = 12f
if (!dir.exists()) return "ERR: path not found" setOnClickListener {
if (!dir.isDirectory) return "ERR: not a directory" selectedChannel = channel
dir.listFiles()?.joinToString("\n") { f -> highlightChannel(channelRow, this)
(if (f.isDirectory) "[DIR] " else "[FILE] ") + f.name + }
(if (f.isFile) " (${f.length()}B)" else "") }
} ?: "empty"
} catch (e: Exception) { "ERR: ${e.message}" }
} }
@android.webkit.JavascriptInterface val btnStable = makeChannelBtn("Stable", "stable")
fun sysinfo(): String { val btnBeta = makeChannelBtn("Beta", "beta")
return """ val btnNightly = makeChannelBtn("Nightly", "nightly")
Device: ${android.os.Build.MODEL}
Android: ${android.os.Build.VERSION.RELEASE} (SDK ${android.os.Build.VERSION.SDK_INT}) channelRow.addView(btnStable)
CPU: ${Runtime.getRuntime().availableProcessors()} cores channelRow.addView(btnBeta)
Memory: ${Runtime.getRuntime().maxMemory() / 1024 / 1024}MB max channelRow.addView(btnNightly)
""".trimIndent() layout.addView(channelRow)
highlightChannel(channelRow, btnStable)
logView = TextView(this).apply {
textSize = 12f
setTextColor(Color.LTGRAY)
}
layout.addView(logView)
val btn = Button(this).apply {
text = "检查更新"
setOnClickListener {
val ch = selectedChannel
CoroutineScope(Dispatchers.IO).launch { doOtaTest(ch) }
} }
} }
layout.addView(btn)
scroll.addView(layout)
setContentView(scroll)
appendLog("渠道: stable | 点击[检查更新]开始")
}
private fun highlightChannel(row: LinearLayout, active: Button) {
for (i in 0 until row.childCount) {
val b = row.getChildAt(i) as Button
b.isSelected = (b == active)
}
}
private fun appendLog(msg: String) {
Log.i(TAG, msg)
runOnUiThread { logView.append("$msg\n") }
}
companion object { private const val TAG = "GCA-OTA" }
private fun manifestUrl(channel: String): String {
val base = "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota"
return when (channel) {
"beta" -> "$base/manifest-beta.json"
"nightly" -> "$base/manifest-nightly.json"
else -> "$base/manifest-stable.json"
}
}
private suspend fun doOtaTest(channel: String) = withContext(Dispatchers.IO) {
val url = manifestUrl(channel)
appendLog("=== OTA 渠道: $channel ===")
appendLog("manifest: $url")
// 1. 拉取 manifest
appendLog("[1/4] 拉取 manifest...")
val manifestJson: String
try {
manifestJson = httpGet(url)
appendLog(" ✅ 获取成功 (${manifestJson.length}B)")
} catch (e: Exception) {
appendLog(" ❌ 获取失败: ${e.message}")
appendLog("=== 测试中止 ===")
return@withContext
}
// 2. 解析
appendLog("[2/4] 解析 manifest...")
var manifestSha256 = ""
var manifestApkUrl = ""
var manifestTag = ""
try {
val json = JSONObject(manifestJson)
if (!json.getJSONObject("builds").has("android")) {
appendLog(" 该渠道暂无可用更新")
appendLog("=== $channel 渠道测试完成 ===")
return@withContext
}
val android = json.getJSONObject("builds").getJSONObject("android")
manifestSha256 = android.getString("sha256")
manifestApkUrl = android.getString("url")
manifestTag = android.getString("tag")
appendLog(" 渠道: $channel 版本: $manifestTag")
appendLog(" SHA256: ${manifestSha256.take(16)}...")
// 版本比较:一致则跳过
val localTag = "v0.1.0-dav-android-${packageManager.getPackageInfo(packageName, 0).versionCode}"
if (manifestTag == localTag) {
appendLog(" ✅ 已是最新版本,无需更新")
appendLog("=== $channel 渠道测试完成 ===")
return@withContext
}
appendLog(" 发现新版本,开始更新...")
} catch (e: Exception) {
appendLog(" ❌ 解析失败: ${e.message}")
return@withContext
}
// 3. 下载
appendLog("[3/4] 下载 APK...")
val apkFile = File(cacheDir, "update-$channel.apk")
try {
downloadFile(manifestApkUrl, apkFile)
appendLog(" ✅ 下载完成: ${apkFile.length()} bytes")
} catch (e: Exception) {
appendLog(" ❌ 下载失败: ${e.message}")
return@withContext
}
// 4. SHA256 校验
appendLog("[4/4] SHA256 校验...")
val actualSha256 = computeSha256(apkFile)
if (actualSha256 == manifestSha256) {
appendLog(" ✅ SHA256 校验通过!")
// 5. 安装 APK
appendLog("[5/5] 提交安装...")
try {
installApk(apkFile)
appendLog(" ✅ 安装已提交,稍后通知栏点击重启")
} catch (e: Exception) {
appendLog(" ❌ 安装失败: ${e.message}")
}
} else {
appendLog(" ❌ SHA256 校验失败!")
appendLog(" expected: ${manifestSha256.take(32)}")
appendLog(" actual: ${actualSha256.take(32)}")
}
appendLog("=== $channel 渠道测试完成 ===")
}
private fun httpGet(url: String): String {
val conn = URL(url).openConnection() as HttpURLConnection
conn.connectTimeout = 10000
conn.readTimeout = 10000
return conn.inputStream.bufferedReader().readText()
}
private val ALLOWED_HOSTS = setOf("git.childish-ghost.com", "github.com")
private fun downloadFile(url: String, dest: File) {
val host = URL(url).host
if (!ALLOWED_HOSTS.any { host == it || host.endsWith(".$it") }) {
throw SecurityException("不允许的下载域名: $host")
}
val conn = URL(url).openConnection() as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 120000
conn.inputStream.use { input ->
FileOutputStream(dest).use { output -> input.copyTo(output) }
}
}
private fun installApk(apkFile: File) {
val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", apkFile)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)
}
private fun computeSha256(file: File): String {
val digest = MessageDigest.getInstance("SHA-256")
file.inputStream().use { input ->
val buf = ByteArray(8192)
var read: Int
while (input.read(buf).also { read = it } != -1) {
digest.update(buf, 0, read)
}
}
return digest.digest().joinToString("") { "%02x".format(it) }
}
} }

View File

@@ -1,3 +0,0 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
</layer-list>

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
>
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="splashscreen_background">#0f172a</color> <color name="primary">#3b82f6</color>
<color name="iconBackground">#0f172a</color> <color name="background">#0f172a</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#0f172a</color>
</resources> </resources>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">YuzuGCA</string> <string name="app_name">Yuzu GCA v0.1.0</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources> </resources>

View File

@@ -1,17 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:textColor">@android:color/black</item>
<item name="android:editTextStyle">@style/ResetEditText</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#0f172a</item>
</style>
<style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item>
<item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
</style>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="apk" path="/" />
</paths>

View File

@@ -12,7 +12,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath('com.android.tools.build:gradle:8.13.2') classpath("com.android.tools.build:gradle:8.7.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
} }
} }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -1,252 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -1,94 +1,26 @@
@rem @echo off
@rem Copyright 2015 the original author or authors. setlocal
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
set APP_BASE_NAME=%~n0
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set ANDROID_HOME=C:\Users\Middl\AppData\Local\Android\Sdk
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if "%ERRORLEVEL%" == "0" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail goto fail
:fail
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
exit /b 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail goto fail
:execute :execute
@rem Setup the command line "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1,2 +1,3 @@
rootProject.name = 'YuzuGCA' rootProject.name = 'YuzuGCA'
include ':app' include ':app'

Some files were not shown because too many files have changed in this diff Show More