mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 09:12:57 +02:00
feat(android): add wrapper sources and fix the address, camera and downloads
The wrapper is what users sideload, so its source belongs next to the APK - they can read what they install, or build it themselves. Behaviour fixes on the way in (APK rebuild pending): - The saved server address survives. Back on the first page and connection errors used to delete it, so a stray tap or a moment without signal meant typing the address again; the form now opens pre-filled and only a save replaces what is stored. Back at the top level asks: exit, change server, or cancel - QR scanning works: the page's camera request is mirrored to an Android permission request (CAMERA, on an https instance - getUserMedia needs a secure context, as in any browser) - Downloads work: a DownloadListener hands database backups and other files to DownloadManager, which puts them in the phone's Downloads folder - Links to other hosts and non-http schemes open in the system browser, so a URL in a message no longer navigates the app away from the instance - Rotating the screen no longer reloads the page Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+10
@@ -80,6 +80,16 @@ data/
|
||||
*.sqlite
|
||||
*.db
|
||||
|
||||
# ============================================
|
||||
# Android wrapper app (android/src)
|
||||
# ============================================
|
||||
.gradle/
|
||||
local.properties
|
||||
# Signing keys never belong in the repository
|
||||
*.jks
|
||||
*.keystore
|
||||
keystore.properties
|
||||
|
||||
# ============================================
|
||||
# OS
|
||||
# ============================================
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Android companion app — source
|
||||
|
||||
The app users install is a thin WebView wrapper around their own mc-webui
|
||||
instance: one screen for the server address, one full-screen WebView for the
|
||||
interface itself. It contains no mesh logic and talks to nothing except the
|
||||
server the user typed in.
|
||||
|
||||
The sources are here so that anyone installing the `.apk` can see what they are
|
||||
installing, and build it themselves if they prefer. For install instructions,
|
||||
see [Android App](../docs/android-app.md).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Published build** | [`mc-webui-wrapper.apk`](mc-webui-wrapper.apk) |
|
||||
| **Package** | `it.wojtaszek.mc.wrapper` |
|
||||
| **Min / target SDK** | 21 (Android 5.0) / 34 |
|
||||
| **Permissions** | `INTERNET`; `CAMERA` for QR scanning; `WRITE_EXTERNAL_STORAGE` (Android 9 and older) for saving downloads |
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
├── settings.gradle.kts, build.gradle.kts, gradle.properties
|
||||
├── gradle/wrapper/gradle-wrapper.properties
|
||||
└── app/
|
||||
├── build.gradle.kts
|
||||
└── src/main/
|
||||
├── AndroidManifest.xml
|
||||
├── java/it/wojtaszek/mc/wrapper/MainActivity.kt ← the whole app
|
||||
└── res/ layout, strings, icons
|
||||
```
|
||||
|
||||
`MainActivity.kt` is the entire application. Worth knowing about it:
|
||||
|
||||
- The server address is stored in `SharedPreferences` and **only ever replaced
|
||||
by the user**. A failed connection or a Back press shows the address form
|
||||
pre-filled — it never wipes what was saved
|
||||
- Back on the first mc-webui page asks: exit, change server, or cancel
|
||||
- Links to other hosts (URLs in messages, the packet analyzer) and non-`http`
|
||||
schemes are handed to the system, so the app stays on the user's instance
|
||||
- The page's camera request (QR scanning) is mirrored to an Android permission
|
||||
request; downloads go to the phone's Downloads folder via `DownloadManager`
|
||||
|
||||
## Building
|
||||
|
||||
Open `src/` in Android Studio (the Gradle wrapper JAR is not checked in —
|
||||
Android Studio supplies it) and let it sync. Then:
|
||||
|
||||
- **Debug build:** *Build → Build Bundle(s) / APK(s) → Build APK(s)* →
|
||||
`app/build/outputs/apk/debug/app-debug.apk`. Fine for trying things out on
|
||||
your own phone, not for publishing — it is signed with the throwaway debug
|
||||
key and is marked debuggable
|
||||
- **Release build:** *Build → Generate Signed App Bundle or APK → APK*, pick
|
||||
the project keystore, choose the `release` variant, and let it build. The
|
||||
result is what ships
|
||||
|
||||
### Signing
|
||||
|
||||
Android identifies an app by its package name **and its signing key**. An
|
||||
update only installs over an existing app when both match, so a release signed
|
||||
with a different key forces every user to uninstall first — losing their saved
|
||||
server address. In practice this means:
|
||||
|
||||
- Use the **same keystore for every release**, from the first published one
|
||||
- **Back it up** (and its passwords) somewhere that survives a reinstalled
|
||||
laptop. There is no way to recover or reissue it
|
||||
- Never commit the keystore or its passwords to this repository
|
||||
|
||||
### Publishing a new build
|
||||
|
||||
1. Bump `versionCode` (and usually `versionName`) in `src/app/build.gradle.kts`
|
||||
2. Build the signed release APK
|
||||
3. Copy it here as `mc-webui-wrapper.apk`
|
||||
4. Update the version, size and **SHA-256** in
|
||||
[`docs/android-app.md`](../docs/android-app.md) — `sha256sum` on Linux,
|
||||
`Get-FileHash` in PowerShell
|
||||
5. Mention the change in [`docs/whatsnew.md`](../docs/whatsnew.md)
|
||||
@@ -0,0 +1,38 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "it.wojtaszek.mc.wrapper"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "it.wojtaszek.mc.wrapper"
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- QR scanning in Add Contact; the page asks first, Android asks second -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- Saving downloads (e.g. database backups) before Android 10 -->
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<!-- A phone without a camera can still do everything else -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,280 @@
|
||||
package it.wojtaszek.mc.wrapper
|
||||
|
||||
import android.Manifest
|
||||
import android.app.DownloadManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.URLUtil
|
||||
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.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/**
|
||||
* A thin wrapper around the user's own mc-webui instance: one screen to enter
|
||||
* the server address, one full-screen WebView for the interface itself.
|
||||
*
|
||||
* The saved address is only ever replaced by the user - a failed connection or
|
||||
* a stray Back press never makes them type it again.
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private lateinit var configLayout: LinearLayout
|
||||
private lateinit var configMessage: TextView
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var urlInput: EditText
|
||||
|
||||
/** Camera request from the page (QR scanning), waiting for the Android permission. */
|
||||
private var pendingCameraRequest: PermissionRequest? = null
|
||||
|
||||
/** Download the page asked for, waiting for the storage permission on older Android. */
|
||||
private var pendingDownload: (() -> Unit)? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
configLayout = findViewById(R.id.configLayout)
|
||||
configMessage = findViewById(R.id.configMessage)
|
||||
webView = findViewById(R.id.webView)
|
||||
urlInput = findViewById(R.id.urlInput)
|
||||
|
||||
setUpWebView()
|
||||
|
||||
findViewById<Button>(R.id.saveButton).setOnClickListener {
|
||||
val typed = urlInput.text.toString().trim()
|
||||
if (typed.isEmpty()) {
|
||||
Toast.makeText(this, R.string.address_empty, Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
// Without a scheme the address is ambiguous; https is the safe default,
|
||||
// and the hint tells local users to type http:// themselves
|
||||
val url = if (typed.startsWith("http://") || typed.startsWith("https://")) typed else "https://$typed"
|
||||
prefs.edit().putString(KEY_URL, url).apply()
|
||||
connect(url)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() = goBack()
|
||||
})
|
||||
|
||||
val savedUrl = prefs.getString(KEY_URL, null)
|
||||
if (savedUrl.isNullOrEmpty()) showConfig(null) else connect(savedUrl)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- screens
|
||||
|
||||
/**
|
||||
* Shows the address form, pre-filled with the saved address. The saved
|
||||
* address itself is left alone, so cancelling out of here changes nothing.
|
||||
*/
|
||||
private fun showConfig(message: String?) {
|
||||
urlInput.setText(prefs.getString(KEY_URL, "") ?: "")
|
||||
configMessage.text = message ?: ""
|
||||
configMessage.visibility = if (message == null) View.GONE else View.VISIBLE
|
||||
configLayout.visibility = View.VISIBLE
|
||||
webView.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun connect(url: String) {
|
||||
configLayout.visibility = View.GONE
|
||||
webView.visibility = View.VISIBLE
|
||||
webView.loadUrl(url)
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
if (configLayout.visibility == View.VISIBLE) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
return
|
||||
}
|
||||
// At the first page Back used to drop the saved address; now it asks
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.app_name)
|
||||
.setMessage(R.string.leave_prompt)
|
||||
.setPositiveButton(R.string.exit) { _, _ -> finish() }
|
||||
.setNeutralButton(R.string.change_server) { _, _ -> showConfig(null) }
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- web view
|
||||
|
||||
private fun setUpWebView() {
|
||||
webView.settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
}
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean =
|
||||
handleUrl(request?.url)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean =
|
||||
handleUrl(if (url == null) null else Uri.parse(url))
|
||||
|
||||
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
||||
if (request?.isForMainFrame == true) showConfig(getString(R.string.error_unreachable))
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
|
||||
handler?.cancel()
|
||||
showConfig(getString(R.string.error_ssl))
|
||||
}
|
||||
}
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
/** QR scanning asks the page for the camera; mirror that to Android. */
|
||||
override fun onPermissionRequest(request: PermissionRequest?) {
|
||||
if (request == null) return
|
||||
runOnUiThread {
|
||||
val wanted = request.resources.filter { it == PermissionRequest.RESOURCE_VIDEO_CAPTURE }
|
||||
when {
|
||||
wanted.isEmpty() -> request.deny()
|
||||
hasPermission(Manifest.permission.CAMERA) -> request.grant(wanted.toTypedArray())
|
||||
else -> {
|
||||
pendingCameraRequest = request
|
||||
ActivityCompat.requestPermissions(
|
||||
this@MainActivity, arrayOf(Manifest.permission.CAMERA), REQ_CAMERA
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Database backups and other files would silently do nothing otherwise
|
||||
webView.setDownloadListener { url, userAgent, contentDisposition, mimeType, _ ->
|
||||
download(url, userAgent, contentDisposition, mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps mc-webui itself inside the app and hands everything else - links in
|
||||
* messages, the packet analyzer, mailto: and friends - to the system.
|
||||
*
|
||||
* @return true when the URL was handled outside the WebView.
|
||||
*/
|
||||
private fun handleUrl(uri: Uri?): Boolean {
|
||||
if (uri == null) return false
|
||||
val scheme = uri.scheme?.lowercase()
|
||||
if (scheme != "http" && scheme != "https") {
|
||||
openExternally(uri)
|
||||
return true
|
||||
}
|
||||
val serverHost = Uri.parse(prefs.getString(KEY_URL, "") ?: "").host
|
||||
if (serverHost != null && !serverHost.equals(uri.host, ignoreCase = true)) {
|
||||
openExternally(uri)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun openExternally(uri: Uri) {
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(this, R.string.no_app_for_link, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- downloads
|
||||
|
||||
private fun download(url: String, userAgent: String?, contentDisposition: String?, mimeType: String?) {
|
||||
// Writing to the public Downloads folder needs permission before Android 10
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
) {
|
||||
pendingDownload = { download(url, userAgent, contentDisposition, mimeType) }
|
||||
ActivityCompat.requestPermissions(
|
||||
this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQ_STORAGE
|
||||
)
|
||||
return
|
||||
}
|
||||
try {
|
||||
val fileName = URLUtil.guessFileName(url, contentDisposition, mimeType)
|
||||
val request = DownloadManager.Request(Uri.parse(url)).apply {
|
||||
setMimeType(mimeType)
|
||||
setTitle(fileName)
|
||||
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFIED)
|
||||
if (!userAgent.isNullOrEmpty()) addRequestHeader("User-Agent", userAgent)
|
||||
CookieManager.getInstance().getCookie(url)?.let { addRequestHeader("Cookie", it) }
|
||||
}
|
||||
(getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).enqueue(request)
|
||||
Toast.makeText(this, getString(R.string.downloading, fileName), Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, getString(R.string.download_failed, e.message ?: ""), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ permissions
|
||||
|
||||
private fun hasPermission(permission: String): Boolean =
|
||||
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
when (requestCode) {
|
||||
REQ_CAMERA -> {
|
||||
val request = pendingCameraRequest
|
||||
pendingCameraRequest = null
|
||||
if (granted) {
|
||||
request?.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
|
||||
} else {
|
||||
request?.deny()
|
||||
Toast.makeText(this, R.string.camera_denied, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
REQ_STORAGE -> {
|
||||
val download = pendingDownload
|
||||
pendingDownload = null
|
||||
if (granted) download?.invoke()
|
||||
else Toast.makeText(this, R.string.storage_denied, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS = "MC_PREFS"
|
||||
private const val KEY_URL = "SERVER_URL"
|
||||
private const val REQ_CAMERA = 1
|
||||
private const val REQ_STORAGE = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#ffffff">
|
||||
|
||||
<!-- Server address form -->
|
||||
<LinearLayout
|
||||
android:id="@+id/configLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
|
||||
<!-- Filled in when a connection fails; hidden otherwise -->
|
||||
<TextView
|
||||
android:id="@+id/configMessage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:textColor="#B00020"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/address_label"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/urlInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/address_hint"
|
||||
android:inputType="textUri"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/address_note"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#666666"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/saveButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/save_and_connect"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- mc-webui itself -->
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,27 @@
|
||||
<resources>
|
||||
<string name="app_name">mc-webui</string>
|
||||
|
||||
<!-- Server address form -->
|
||||
<string name="address_label">Enter your server address:</string>
|
||||
<string name="address_hint">e.g. https://your-server-ip-or-domain</string>
|
||||
<string name="address_note">Note: Use https:// for secure connection. If you are on a local network without SSL, you can use http:// (e.g., http://192.168.1.100).</string>
|
||||
<string name="save_and_connect">SAVE & CONNECT</string>
|
||||
<string name="address_empty">Address cannot be empty</string>
|
||||
|
||||
<!-- Leaving the app -->
|
||||
<string name="leave_prompt">Leave mc-webui, or connect to a different server?</string>
|
||||
<string name="exit">Exit</string>
|
||||
<string name="change_server">Change server</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
|
||||
<!-- Connection problems -->
|
||||
<string name="error_unreachable">Could not reach the server. Check the address, and that the phone is on the right network.</string>
|
||||
<string name="error_ssl">SSL error - the certificate was rejected. Use a valid HTTPS certificate, or plain http:// on a local network.</string>
|
||||
<string name="no_app_for_link">No app on the phone can open this link</string>
|
||||
|
||||
<!-- Downloads and permissions -->
|
||||
<string name="downloading">Downloading %1$s</string>
|
||||
<string name="download_failed">Download failed: %1$s</string>
|
||||
<string name="camera_denied">Camera permission denied - QR scanning needs it</string>
|
||||
<string name="storage_denied">Storage permission denied - the file cannot be saved</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.2.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
|
||||
}
|
||||
}
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,2 @@
|
||||
rootProject.name = "mc-webui-wrapper"
|
||||
include(":app")
|
||||
@@ -161,6 +161,10 @@ full-screen WebView for mc-webui itself, and a saved preference between them.
|
||||
No analytics, no third-party services, no background activity — when the app is
|
||||
closed, nothing of it runs.
|
||||
|
||||
The complete source is in [`android/src/`](../android/src), and
|
||||
[`android/README.md`](../android/README.md) describes how to build it yourself
|
||||
if you would rather not trust a prebuilt `.apk`.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
Reference in New Issue
Block a user