feat: android app list page with search and icons
This commit is contained in:
@@ -59,3 +59,43 @@ body {
|
|||||||
|
|
||||||
.page { display: none; height: 100%; }
|
.page { display: none; height: 100%; }
|
||||||
.page.active { display: flex; flex-direction: column; }
|
.page.active { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
/* 安卓 APP 列表页:搜索框 + 网格 */
|
||||||
|
/* Android app list page: search box + grid */
|
||||||
|
.search { padding: 16px; }
|
||||||
|
.search input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
background: var(--glass-bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell .ic { width: 48px; height: 48px; border-radius: 12px; }
|
||||||
|
.cell .lbl { font-size: 12px; color: var(--text-dim); text-align: center; }
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<script src="js/bridge.js"></script>
|
<script src="js/bridge.js"></script>
|
||||||
<script src="js/router.js"></script>
|
<script src="js/router.js"></script>
|
||||||
|
<script src="js/pages/applist.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 渲染侧边栏导航项 / render sidebar nav items
|
// 渲染侧边栏导航项 / render sidebar nav items
|
||||||
const nav = document.getElementById('rail-nav');
|
const nav = document.getElementById('rail-nav');
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 安卓 APP 列表页:网格 + 搜索 + 图标 base64
|
||||||
|
// Android app list page: grid + search + icon base64
|
||||||
|
window.loadAppList = async function () {
|
||||||
|
const page = document.querySelector('[data-page="applist"]');
|
||||||
|
if (page.dataset.loaded) return; page.dataset.loaded = '1';
|
||||||
|
page.innerHTML = `
|
||||||
|
<div class="search"><input id="app-search" placeholder="搜索应用…"></div>
|
||||||
|
<div class="grid" id="app-grid"></div>`;
|
||||||
|
const apps = JSON.parse(await bridge.call('listApps'));
|
||||||
|
const grid = document.getElementById('app-grid');
|
||||||
|
const render = (list) => {
|
||||||
|
grid.innerHTML = list.map(a => `
|
||||||
|
<button class="cell" data-pkg="${a.packageName}">
|
||||||
|
<img class="ic" src="${a.iconBase64 || ''}" alt="">
|
||||||
|
<span class="lbl">${a.label}</span>
|
||||||
|
</button>`).join('');
|
||||||
|
grid.querySelectorAll('.cell').forEach(el => el.onclick = () => bridge.call('launchApp', el.dataset.pkg));
|
||||||
|
};
|
||||||
|
render(apps);
|
||||||
|
document.getElementById('app-search').oninput = (e) => {
|
||||||
|
const kw = e.target.value.toLowerCase();
|
||||||
|
render(apps.filter(a => a.label.toLowerCase().includes(kw)));
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -6,6 +6,8 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
|
import top.yeij.hearth.app.AndroidAppSource
|
||||||
|
import top.yeij.hearth.app.AppRepository
|
||||||
import top.yeij.hearth.webview.JsBridge
|
import top.yeij.hearth.webview.JsBridge
|
||||||
import top.yeij.hearth.webview.WebViewManager
|
import top.yeij.hearth.webview.WebViewManager
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ class MainActivity : Activity() {
|
|||||||
deviceHeightPx = dm.heightPixels,
|
deviceHeightPx = dm.heightPixels,
|
||||||
density = dm.density,
|
density = dm.density,
|
||||||
darkMode = darkMode,
|
darkMode = darkMode,
|
||||||
|
appRepository = AppRepository(AndroidAppSource(this)),
|
||||||
)
|
)
|
||||||
WebViewManager(this).attach(bridge)
|
WebViewManager(this).attach(bridge)
|
||||||
Log.d(
|
Log.d(
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ package top.yeij.hearth.app
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
data class AppInfo(val packageName: String, val label: String, val iconBase64: String?)
|
data class AppInfo(val packageName: String, val label: String, val iconBase64: String?)
|
||||||
|
|
||||||
@@ -17,7 +22,7 @@ class AndroidAppSource(private val context: Context) : AppSource {
|
|||||||
val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
return pm.queryIntentActivities(intent, 0).map { ri ->
|
return pm.queryIntentActivities(intent, 0).map { ri ->
|
||||||
val pkg = ri.activityInfo.packageName
|
val pkg = ri.activityInfo.packageName
|
||||||
AppInfo(pkg, ri.loadLabel(pm).toString(), null /* 图标 base64 在 Task 5 补 */)
|
AppInfo(pkg, ri.loadLabel(pm).toString(), drawableToBase64(ri.loadIcon(pm)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +32,18 @@ class AndroidAppSource(private val context: Context) : AppSource {
|
|||||||
context.startActivity(i)
|
context.startActivity(i)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 图标 Drawable → 96px PNG base64 字符串(含 data:image/png;base64, 前缀)
|
||||||
|
// Convert app icon Drawable to a 96px PNG base64 string (with data:image/png;base64, prefix)
|
||||||
|
private fun drawableToBase64(d: Drawable, size: Int = 96): String {
|
||||||
|
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bmp)
|
||||||
|
d.setBounds(0, 0, size, size)
|
||||||
|
d.draw(canvas)
|
||||||
|
val out = ByteArrayOutputStream()
|
||||||
|
bmp.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||||
|
return "data:image/png;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppRepository(private val source: AppSource) {
|
class AppRepository(private val source: AppSource) {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package top.yeij.hearth.webview
|
package top.yeij.hearth.webview
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import top.yeij.hearth.app.AppRepository
|
||||||
|
|
||||||
class JsBridge(
|
class JsBridge(
|
||||||
private val deviceWidthPx: Int,
|
private val deviceWidthPx: Int,
|
||||||
private val deviceHeightPx: Int,
|
private val deviceHeightPx: Int,
|
||||||
private val density: Float,
|
private val density: Float,
|
||||||
private val darkMode: Boolean,
|
private val darkMode: Boolean,
|
||||||
|
private val appRepository: AppRepository? = null,
|
||||||
) {
|
) {
|
||||||
private val gson = com.google.gson.Gson()
|
private val gson = com.google.gson.Gson()
|
||||||
|
|
||||||
@@ -22,4 +24,14 @@ class JsBridge(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 返回已安装应用列表 JSON(无仓库时返回空数组)
|
||||||
|
// Return installed app list JSON (empty array when no repository wired)
|
||||||
|
@android.webkit.JavascriptInterface
|
||||||
|
fun listApps(): String = appRepository?.listApps() ?: "[]"
|
||||||
|
|
||||||
|
// 启动指定包名应用,成功返回 true(无仓库时返回 false)
|
||||||
|
// Launch an app by package name, true on success (false when no repository wired)
|
||||||
|
@android.webkit.JavascriptInterface
|
||||||
|
fun launchApp(packageName: String): Boolean = appRepository?.launchApp(packageName) ?: false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user