🎉 初始化 EasyAppShell — 配置化 Android WebView 壳项目

- 配置驱动的 WebView App 壳 (Kotlin)
- auto_package.py 一键打包脚本(改 URL/名称/图标/包名)
- 完整 WebView 功能:JS/DOM存储/定位/下拉刷新/进度条
- JS↔Native 桥接 (Toast/分享/剪贴板)
- 自适应图标 + 多分辨率图标支持
- Gradle 8.10 + AGP 8.7.3 + Kotlin 2.1.0
This commit is contained in:
Tatta
2026-07-02 22:52:37 +08:00
commit 2e93f8c282
38 changed files with 1823 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.easyappshell.memos"
compileSdk = 36
defaultConfig {
applicationId = "com.easyappshell.memos"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0.0"
}
buildFeatures {
buildConfig = true
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.webkit)
implementation(libs.androidx.appcompat)
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
}
+10
View File
@@ -0,0 +1,10 @@
# ProGuard rules for EasyAppShell
# WebView 相关
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebChromeClient {
public void *(android.webkit.WebView, java.lang.String);
}
-dontwarn com.easyappshell.**
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 可选权限(根据 config.json 启用) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.EasyAppShell"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="false">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 支持深度链接:通过 URL 启动并加载指定链接 -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="*" />
</intent-filter>
</activity>
</application>
</manifest>
+31
View File
@@ -0,0 +1,31 @@
{
"app_name": "Memos",
"main_url": "https://memo.pags.cn",
"load_error_page": "file:///android_asset/error.html",
"enable_js": true,
"enable_dom_storage": true,
"enable_file_access": false,
"allow_javascript_interface": true,
"support_multiple_windows": false,
"user_agent_suffix": "EasyAppShell",
"status_bar_color": "#2196F3",
"status_bar_light_icons": true,
"orientation": "unspecified",
"show_loading_progress": true,
"enable_swipe_refresh": true,
"clear_cache_on_start": false,
"allow_http": false,
"allow_content_url": false,
"enable_geolocation": true,
"geolocation_api_host": "",
"extra_http_headers": {},
"allowed_origin_whitelist": [
"*"
],
"blocked_hosts": [],
"pull_to_refresh_colors": [
"#2196F3",
"#03A9F4",
"#00BCD4"
]
}
+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加载失败</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f5f5f5;
color: #333;
padding: 20px;
}
.container {
text-align: center;
max-width: 360px;
}
.icon {
font-size: 64px;
margin-bottom: 16px;
}
h1 {
font-size: 20px;
margin-bottom: 8px;
}
p {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 24px;
}
.btn {
display: inline-block;
padding: 12px 32px;
background: #2196F3;
color: #fff;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
text-decoration: none;
}
.btn:active {
opacity: 0.8;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">📡</div>
<h1>网络开小差了</h1>
<p>页面加载失败,请检查网络连接后重试。</p>
<button class="btn" onclick="reloadPage()">重新加载</button>
</div>
<script>
function reloadPage() {
if (window.NativeBridge && NativeBridge.reloadPage) {
NativeBridge.reloadPage();
} else {
// fallback: 通过 Android 回调重新加载
document.location.reload();
}
}
</script>
</body>
</html>
@@ -0,0 +1,335 @@
package com.easyappshell.memos
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.webkit.GeolocationPermissions
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
class MainActivity : AppCompatActivity() {
private lateinit var config: AppConfig
private lateinit var webView: WebView
private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var progressBar: ProgressBar
private lateinit var errorView: TextView
private lateinit var webViewContainer: FrameLayout
private var pageLoadTimeout: Handler? = null
private val pageLoadTimeoutRunnable = Runnable {
showError(-1, "连接超时,请检查网络后重试")
}
override fun onCreate(savedInstanceState: Bundle?) {
pageLoadTimeout = Handler(Looper.getMainLooper())
super.onCreate(savedInstanceState)
config = AppConfig.load(this)
title = config.appName
setOrientation(config.orientation)
setupStatusBar()
setContentView(R.layout.activity_main)
initViews()
setupWebView()
setupErrorViewRetry()
loadMainUrl()
}
@SuppressLint("SetJavaScriptEnabled")
private fun initViews() {
webViewContainer = findViewById(R.id.webViewContainer)
swipeRefresh = findViewById(R.id.swipeRefresh)
progressBar = findViewById(R.id.progressBar)
errorView = findViewById(R.id.errorView)
webView = findViewById(R.id.webView)
swipeRefresh.isEnabled = config.enableSwipeRefresh
swipeRefresh.setOnRefreshListener { webView.reload() }
if (config.pullToRefreshColors.isNotEmpty()) {
val colorInts = config.pullToRefreshColors.map { Color.parseColor(it) }.toIntArray()
swipeRefresh.setColorSchemeColors(*colorInts)
}
progressBar.visibility = if (config.showLoadingProgress) View.VISIBLE else View.GONE
ContextCompat.getDrawable(this, android.R.color.holo_blue_bright)?.let {
progressBar.indeterminateDrawable = it
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
val settings = webView.settings
settings.javaScriptEnabled = config.enableJs
settings.domStorageEnabled = config.enableDomStorage
settings.allowFileAccess = config.enableFileAccess
settings.allowContentAccess = config.allowContentUrl
settings.javaScriptCanOpenWindowsAutomatically = config.supportMultipleWindows
settings.setSupportMultipleWindows(config.supportMultipleWindows)
settings.loadWithOverviewMode = true
settings.useWideViewPort = true
settings.builtInZoomControls = true
settings.displayZoomControls = false
settings.cacheMode = WebSettings.LOAD_DEFAULT
settings.mixedContentMode = if (config.allowHttp)
WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
else
WebSettings.MIXED_CONTENT_NEVER_ALLOW
if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
WebViewCompat.startSafeBrowsing(this, object : ValueCallback<Boolean> {
override fun onReceiveValue(value: Boolean?) {}
})
}
if (config.userAgentSuffix.isNotBlank()) {
val ua = webView.settings.userAgentString
if (!ua.contains(config.userAgentSuffix)) {
webView.settings.userAgentString = "$ua ${config.userAgentSuffix}"
}
}
settings.mediaPlaybackRequiresUserGesture = true
if (config.allowedOriginWhitelist.isNotEmpty()) {
val origins = config.allowedOriginWhitelist.toTypedArray()
if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ALLOWLIST)) {
WebViewCompat.setSafeBrowsingAllowlist(origins.toSet(), object : ValueCallback<Boolean> {
override fun onReceiveValue(value: Boolean?) {}
})
}
}
if (config.clearCacheOnStart) {
webView.clearCache(true)
webView.clearHistory()
}
webView.webViewClient = EasyWebViewClient()
webView.webChromeClient = EasyChromeClient()
if (config.allowJavascriptInterface) {
webView.addJavascriptInterface(NativeBridge(this), "NativeBridge")
}
}
private fun loadMainUrl() {
val urlToLoad = intent?.dataString ?: config.mainUrl
if (urlToLoad.isNotBlank()) {
startPageLoadTimeout()
webView.loadUrl(urlToLoad, config.extraHttpHeaders)
}
}
private fun startPageLoadTimeout() {
pageLoadTimeout?.removeCallbacks(pageLoadTimeoutRunnable)
pageLoadTimeout?.postDelayed(pageLoadTimeoutRunnable, 15000L)
}
private fun cancelPageLoadTimeout() {
pageLoadTimeout?.removeCallbacks(pageLoadTimeoutRunnable)
}
private fun showError(code: Int, description: String) {
swipeRefresh.isRefreshing = false
progressBar.visibility = View.GONE
webView.visibility = View.GONE
webViewContainer.visibility = View.GONE
errorView.visibility = View.VISIBLE
errorView.text = getString(R.string.error_loading_page, code, description)
}
private fun setupErrorViewRetry() {
errorView.setOnClickListener {
retryLoad()
}
}
private fun retryLoad() {
errorView.visibility = View.GONE
webView.visibility = View.VISIBLE
webViewContainer.visibility = View.VISIBLE
loadMainUrl()
}
private fun setOrientation(orientation: String) {
requestedOrientation = when (orientation.lowercase()) {
"portrait" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
"landscape" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
"sensor_portrait" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
"sensor_landscape" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
private fun setupStatusBar() {
val colorInt = try {
Color.parseColor(config.statusBarColor)
} catch (_: Exception) {
Color.parseColor("#2196F3")
}
window.statusBarColor = colorInt
WindowCompat.getInsetsController(window, window.decorView).also {
it.isAppearanceLightStatusBars = !config.statusBarLightIcons
}
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
inner class EasyWebViewClient : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
cancelPageLoadTimeout()
errorView.visibility = View.GONE
webView.visibility = View.VISIBLE
webViewContainer.visibility = View.VISIBLE
if (config.showLoadingProgress) progressBar.visibility = View.VISIBLE
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
cancelPageLoadTimeout()
swipeRefresh.isRefreshing = false
if (config.showLoadingProgress) progressBar.visibility = View.GONE
}
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
cancelPageLoadTimeout()
if (request?.isForMainFrame == true) {
val errorCode = error?.errorCode ?: -1
val description = error?.description?.toString() ?: "未知错误"
showError(errorCode, description)
}
}
override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: android.webkit.WebResourceResponse?) {
if (request?.isForMainFrame == true) {
cancelPageLoadTimeout()
val statusCode = errorResponse?.statusCode ?: -1
val reasonPhrase = errorResponse?.reasonPhrase ?: "HTTP Error"
showError(statusCode, "$statusCode $reasonPhrase")
}
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val url = request?.url?.toString() ?: return false
request?.url?.host?.let { host ->
if (config.blockedHosts.any { host.contains(it, ignoreCase = true) }) return true
}
return when {
url.startsWith("tel:") || url.startsWith("mailto:") ||
url.startsWith("sms:") || url.startsWith("geo:") -> {
try { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } catch (_: Exception) {}
true
}
else -> false
}
}
}
inner class EasyChromeClient : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
if (config.showLoadingProgress && progressBar.visibility == View.VISIBLE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
progressBar.setProgress(newProgress, true)
} else {
progressBar.progress = newProgress
}
}
}
override fun onGeolocationPermissionsShowPrompt(origin: String?, callback: GeolocationPermissions.Callback?) {
if (config.enableGeolocation) {
callback?.invoke(origin, true, false)
} else {
super.onGeolocationPermissionsShowPrompt(origin, callback)
}
}
override fun onCreateWindow(view: WebView?, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?): Boolean {
if (config.supportMultipleWindows && resultMsg != null) {
val newWV = WebView(this@MainActivity)
newWV.settings.javaScriptEnabled = config.enableJs
newWV.webViewClient = EasyWebViewClient()
(view?.parent as? ViewGroup)?.addView(newWV)
resultMsg.obj = newWV
return true
}
return false
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
webView.goBack()
return true
}
return super.onKeyDown(keyCode, event)
}
override fun onBackPressed() {
if (webView.canGoBack()) webView.goBack() else super.onBackPressed()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
webView.saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
webView.restoreState(savedInstanceState)
}
override fun onResume() {
super.onResume()
webView.onResume()
webView.resumeTimers()
}
override fun onPause() {
super.onPause()
webView.onPause()
webView.pauseTimers()
}
override fun onDestroy() {
webViewContainer.removeView(webView)
webView.removeAllViews()
webView.destroy()
super.onDestroy()
}
}
@@ -0,0 +1,43 @@
package com.easyappshell.memos
import android.app.Activity
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.webkit.JavascriptInterface
import android.widget.Toast
class NativeBridge(private val context: Context) {
@JavascriptInterface
fun showToast(message: String) {
(context as? Activity)?.runOnUiThread {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface
fun shareUrl(title: String, url: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, title)
putExtra(Intent.EXTRA_TEXT, url)
}
(context as? Activity)?.runOnUiThread {
context.startActivity(Intent.createChooser(intent, "分享"))
}
}
@JavascriptInterface
fun getAppVersion(): String = "1.0.0"
@JavascriptInterface
fun copyToClipboard(text: String) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
as android.content.ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("label", text))
(context as? Activity)?.runOnUiThread {
Toast.makeText(context, "已复制到剪贴板", Toast.LENGTH_SHORT).show()
}
}
}
@@ -0,0 +1,87 @@
package com.easyappshell.memos
import android.content.Context
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.nio.charset.Charset
data class AppConfig(
val appName: String,
val mainUrl: String,
val loadErrorPage: String,
val enableJs: Boolean,
val enableDomStorage: Boolean,
val enableFileAccess: Boolean,
val allowJavascriptInterface: Boolean,
val supportMultipleWindows: Boolean,
val userAgentSuffix: String,
val statusBarColor: String,
val statusBarLightIcons: Boolean,
val orientation: String,
val showLoadingProgress: Boolean,
val enableSwipeRefresh: Boolean,
val clearCacheOnStart: Boolean,
val allowHttp: Boolean,
val allowContentUrl: Boolean,
val enableGeolocation: Boolean,
val geolocationApiHost: String,
val extraHttpHeaders: Map<String, String>,
val allowedOriginWhitelist: List<String>,
val blockedHosts: List<String>,
val pullToRefreshColors: List<String>
) {
companion object {
private const val CONFIG_FILE = "config.json"
fun load(context: Context): AppConfig {
val jsonString = try {
context.assets.open(CONFIG_FILE)
.bufferedReader(Charset.forName("UTF-8"))
.use { it.readText() }
} catch (e: IOException) {
throw IllegalStateException("无法读取 $CONFIG_FILE: ${e.message}", e)
}
return try {
val json = JSONObject(jsonString)
parse(json)
} catch (e: JSONException) {
throw IllegalStateException("解析 $CONFIG_FILE 失败: ${e.message}", e)
}
}
private fun parse(json: JSONObject): AppConfig = AppConfig(
appName = json.optString("app_name", "EasyAppShell"),
mainUrl = json.optString("main_url", "https://example.com"),
loadErrorPage = json.optString("load_error_page", "file:///android_asset/error.html"),
enableJs = json.optBoolean("enable_js", true),
enableDomStorage = json.optBoolean("enable_dom_storage", true),
enableFileAccess = json.optBoolean("enable_file_access", false),
allowJavascriptInterface = json.optBoolean("allow_javascript_interface", true),
supportMultipleWindows = json.optBoolean("support_multiple_windows", false),
userAgentSuffix = json.optString("user_agent_suffix", "EasyAppShell"),
statusBarColor = json.optString("status_bar_color", "#2196F3"),
statusBarLightIcons = json.optBoolean("status_bar_light_icons", true),
orientation = json.optString("orientation", "unspecified"),
showLoadingProgress = json.optBoolean("show_loading_progress", true),
enableSwipeRefresh = json.optBoolean("enable_swipe_refresh", true),
clearCacheOnStart = json.optBoolean("clear_cache_on_start", false),
allowHttp = json.optBoolean("allow_http", false),
allowContentUrl = json.optBoolean("allow_content_url", false),
enableGeolocation = json.optBoolean("enable_geolocation", true),
geolocationApiHost = json.optString("geolocation_api_host", ""),
extraHttpHeaders = json.optJSONObject("extra_http_headers")
?.let { obj -> obj.keys().asSequence().associate { it to obj.getString(it) } }
?: emptyMap(),
allowedOriginWhitelist = json.optJSONArray("allowed_origin_whitelist")
?.let { arr -> (0 until arr.length()).map { arr.getString(it) } }
?: listOf("*"),
blockedHosts = json.optJSONArray("blocked_hosts")
?.let { arr -> (0 until arr.length()).map { arr.getString(it) } }
?: emptyList(),
pullToRefreshColors = json.optJSONArray("pull_to_refresh_colors")
?.let { arr -> (0 until arr.length()).map { arr.getString(it) } }
?: listOf("#2196F3")
)
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#2196F3"
android:pathData="M0,0h108v108h-108z" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 加载进度条 -->
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
android:max="100"
android:progress="0"
android:visibility="gone"
android:indeterminate="false" />
<!-- WebView 容器 -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/webViewContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 错误页(加载失败时显示) -->
<TextView
android:id="@+id/errorView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="32dp"
android:textSize="14sp"
android:textColor="#666666"
android:visibility="gone"
android:drawableTop="@android:drawable/ic_dialog_alert"
android:drawablePadding="16dp"
android:lineSpacingExtra="4dp" />
</LinearLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 夜间模式主题 -->
<style name="Theme.EasyAppShell" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<item name="android:windowBackground">@color/background_dark</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
</style>
</resources>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 主题色 - 仅供参考,实际颜色由 config.json 控制 -->
<color name="primary">#2196F3</color>
<color name="primary_dark">#1976D2</color>
<color name="accent">#03A9F4</color>
<!-- 背景 -->
<color name="background_light">#FFFFFF</color>
<color name="background_dark">#121212</color>
<!-- 文字 -->
<color name="text_primary">#212121</color>
<color name="text_secondary">#757575</color>
<!-- 进度条 -->
<color name="progress_bar">#2196F3</color>
</resources>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- App 名称(会被 config.json 中的 app_name 覆盖) -->
<string name="app_name">Memos</string>
<!-- 通用 -->
<string name="loading">加载中…</string>
<!-- 错误页 -->
<string name="error_loading_page">加载失败 (错误码: %1$d)\n%2$s</string>
<!-- 分享 -->
<string name="share_title">分享</string>
<!-- 剪贴板 -->
<string name="copied">已复制到剪贴板</string>
</resources>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 基础主题 -->
<style name="Theme.EasyAppShell" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<item name="android:windowBackground">@color/background_light</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
</style>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
网络安全配置
允许明文 HTTP 访问(如果 config.json 中 allow_http = true 则需要放开)
同时也允许加载本地 asset 中的内容
-->
<network-security-config>
<!-- 默认情况下只允许 HTTPS -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- 本地 Asset 允许加载 -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
<!-- 如果 App 需要加载 HTTP 站点,请在 build 脚本中动态替换此文件 -->
</network-security-config>