feat: log export to Download
This commit is contained in:
@@ -42,6 +42,10 @@ window.loadSettings = function () {
|
|||||||
<span class="arrow" id="check-update-state">›</span>
|
<span class="arrow" id="check-update-state">›</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="st-group">关于</div>
|
<div class="st-group">关于</div>
|
||||||
|
<div class="st-item" id="export-log-item">
|
||||||
|
<span class="lbl">导出日志</span>
|
||||||
|
<span class="arrow" id="export-log-state">›</span>
|
||||||
|
</div>
|
||||||
<div class="st-item"><span class="lbl">版本 0.1.0</span><span class="arrow">›</span></div>
|
<div class="st-item"><span class="lbl">版本 0.1.0</span><span class="arrow">›</span></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
setupTheme(page);
|
setupTheme(page);
|
||||||
@@ -51,6 +55,7 @@ window.loadSettings = function () {
|
|||||||
setupBrightness(page);
|
setupBrightness(page);
|
||||||
setupServerUrl(page);
|
setupServerUrl(page);
|
||||||
setupCheckUpdate(page);
|
setupCheckUpdate(page);
|
||||||
|
setupExportLog(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 判断当前是否深色主题(与 mask.js 逻辑一致)
|
// 判断当前是否深色主题(与 mask.js 逻辑一致)
|
||||||
@@ -238,6 +243,21 @@ function setupCheckUpdate(page) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出日志:点击导出到 Download,显示结果
|
||||||
|
// Export log: tap to export to Download, show the result
|
||||||
|
function setupExportLog(page) {
|
||||||
|
const item = page.querySelector('#export-log-item');
|
||||||
|
const state = page.querySelector('#export-log-state');
|
||||||
|
item.onclick = () => {
|
||||||
|
state.textContent = '导出中…';
|
||||||
|
bridge.call('exportLog').then(result => {
|
||||||
|
state.textContent = result + ' ›';
|
||||||
|
}).catch(() => {
|
||||||
|
state.textContent = '失败 ›';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 通用滑块拖动:touch 拖动实时回调(保留 click 兼容桌面调试)
|
// 通用滑块拖动:touch 拖动实时回调(保留 click 兼容桌面调试)
|
||||||
// Generic draggable slider: touch-drag with live callback (click kept for desktop)
|
// Generic draggable slider: touch-drag with live callback (click kept for desktop)
|
||||||
function makeSliderDraggable(slider, onRatio) {
|
function makeSliderDraggable(slider, onRatio) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package top.yeij.hearth
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.WallpaperManager
|
import android.app.WallpaperManager
|
||||||
|
import android.content.ContentValues
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@@ -10,6 +11,7 @@ import android.graphics.Color
|
|||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
@@ -215,6 +217,7 @@ class MainActivity : Activity() {
|
|||||||
onShowTabPanel = { showNativeTabPanel() },
|
onShowTabPanel = { showNativeTabPanel() },
|
||||||
storage = webAppStorage,
|
storage = webAppStorage,
|
||||||
onImportOfflinePackage = { id -> startImportOfflinePackage(id) },
|
onImportOfflinePackage = { id -> startImportOfflinePackage(id) },
|
||||||
|
onExportLog = { exportLog() },
|
||||||
)
|
)
|
||||||
root = FrameLayout(this)
|
root = FrameLayout(this)
|
||||||
setContentView(root)
|
setContentView(root)
|
||||||
@@ -436,6 +439,34 @@ class MainActivity : Activity() {
|
|||||||
// dp to px
|
// dp to px
|
||||||
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
|
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
|
||||||
|
|
||||||
|
// 导出日志到 Download 目录(读 logcat 本进程 + 过滤 Hearth 相关行)
|
||||||
|
// Export the log to the Download directory (read logcat, filter Hearth lines)
|
||||||
|
private fun exportLog(): String {
|
||||||
|
return try {
|
||||||
|
val process = Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
|
||||||
|
val log = process.inputStream.bufferedReader().readText()
|
||||||
|
val filtered = log.lines()
|
||||||
|
.filter { it.contains("Hearth") }
|
||||||
|
.joinToString("\n")
|
||||||
|
val name = "hearth-log-" + System.currentTimeMillis() + ".txt"
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(MediaStore.Downloads.DISPLAY_NAME, name)
|
||||||
|
put(MediaStore.Downloads.MIME_TYPE, "text/plain")
|
||||||
|
put(MediaStore.Downloads.RELATIVE_PATH, "Download/")
|
||||||
|
}
|
||||||
|
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||||
|
?: return "导出失败:无法创建文件"
|
||||||
|
contentResolver.openOutputStream(uri)?.use { out ->
|
||||||
|
out.write(filtered.toByteArray())
|
||||||
|
} ?: return "导出失败:无法写入"
|
||||||
|
Log.d(TAG, "exportLog: $name (${filtered.length} chars)")
|
||||||
|
"已导出:Download/$name"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "exportLog failed: ${e.message}")
|
||||||
|
"导出失败:${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 触发 SAF 选择 zip 导入离线包
|
// 触发 SAF 选择 zip 导入离线包
|
||||||
// Trigger SAF to pick a zip for offline-package import
|
// Trigger SAF to pick a zip for offline-package import
|
||||||
private fun startImportOfflinePackage(id: String) {
|
private fun startImportOfflinePackage(id: String) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class WebAppRepository(
|
|||||||
|
|
||||||
fun fetchManifest(): List<WebApp> {
|
fun fetchManifest(): List<WebApp> {
|
||||||
val body = http.get(manifestUrl)
|
val body = http.get(manifestUrl)
|
||||||
|
Log.d(TAG, "fetchManifest: url=$manifestUrl bodyNull=${body == null}")
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
// 先解析、成功才缓存,避免畸形 body 或空结果被永久缓存
|
// 先解析、成功才缓存,避免畸形 body 或空结果被永久缓存
|
||||||
// Parse first, cache only on success (never cache malformed/empty bodies)
|
// Parse first, cache only on success (never cache malformed/empty bodies)
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ class JsBridge(
|
|||||||
// 手动导入离线包回调:由 MainActivity 实现(SAF 选择 zip 后导入)
|
// 手动导入离线包回调:由 MainActivity 实现(SAF 选择 zip 后导入)
|
||||||
// Manual offline-package import callback implemented by MainActivity (SAF pick + import)
|
// Manual offline-package import callback implemented by MainActivity (SAF pick + import)
|
||||||
private val onImportOfflinePackage: ((String) -> Unit)? = null,
|
private val onImportOfflinePackage: ((String) -> Unit)? = null,
|
||||||
|
// 导出日志回调:由 MainActivity 实现,返回导出结果描述(文件名/错误)
|
||||||
|
// Export-log callback implemented by MainActivity, returns a result description
|
||||||
|
private val onExportLog: (() -> String)? = null,
|
||||||
) {
|
) {
|
||||||
private val gson = com.google.gson.Gson()
|
private val gson = com.google.gson.Gson()
|
||||||
|
|
||||||
@@ -185,6 +188,14 @@ class JsBridge(
|
|||||||
@android.webkit.JavascriptInterface
|
@android.webkit.JavascriptInterface
|
||||||
fun hasLocalPackage(id: String): Boolean = storage?.hasLocalPackage(id) ?: false
|
fun hasLocalPackage(id: String): Boolean = storage?.hasLocalPackage(id) ?: false
|
||||||
|
|
||||||
|
// 导出日志到 Download 目录,返回结果描述(文件名或错误)
|
||||||
|
// Export the log to the Download directory, return a result description
|
||||||
|
@android.webkit.JavascriptInterface
|
||||||
|
fun exportLog(): String {
|
||||||
|
Log.d("HearthBridge", "exportLog called")
|
||||||
|
return onExportLog?.invoke() ?: "日志导出未实现"
|
||||||
|
}
|
||||||
|
|
||||||
// 本地离线包版本号(无则空字符串)
|
// 本地离线包版本号(无则空字符串)
|
||||||
// Local offline-package version (empty string when absent)
|
// Local offline-package version (empty string when absent)
|
||||||
@android.webkit.JavascriptInterface
|
@android.webkit.JavascriptInterface
|
||||||
|
|||||||
Reference in New Issue
Block a user