feat(kiosk): add setup wizard on first launch
- 3-step wizard: Welcome → Server URL → Scale Setup → Launch - Connection test with live feedback - BLE scale status during wizard - Dark theme with modern UI (slate/purple palette) - Settings page with URL edit, connection test, scale info, wizard reset - Skip scale option for users without BLE scales - Error page with retry button when server unreachable - All UI in English
This commit is contained in:
+409
-170
@@ -2,9 +2,13 @@ package it.dadaloop.evershelf.kiosk
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.*
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -13,100 +17,408 @@ import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.webkit.*
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import it.dadaloop.evershelf.kiosk.databinding.ActivityKioskBinding
|
||||
|
||||
private const val PREFS_NAME = "evershelf_kiosk"
|
||||
private const val PREF_URL = "evershelf_url"
|
||||
private const val DEFAULT_URL = "http://evershelf.local"
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
class KioskActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityKioskBinding
|
||||
private var gatewayService: ScaleGatewayService? = null
|
||||
private var bound = false
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private var currentStep = 1
|
||||
|
||||
// Views
|
||||
private lateinit var wizardContainer: ScrollView
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var btnSettings: ImageButton
|
||||
private lateinit var step1: LinearLayout
|
||||
private lateinit var step2: LinearLayout
|
||||
private lateinit var step3: LinearLayout
|
||||
private lateinit var stepIndicator: LinearLayout
|
||||
private lateinit var wizardUrl: EditText
|
||||
private lateinit var urlStatus: TextView
|
||||
private lateinit var scaleStatusIcon: TextView
|
||||
private lateinit var scaleStatusText: TextView
|
||||
private lateinit var scaleStatusDetail: TextView
|
||||
|
||||
// Scale service
|
||||
private var scaleService: ScaleGatewayService? = null
|
||||
private var serviceBound = false
|
||||
private val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
val binder = service as ScaleGatewayService.LocalBinder
|
||||
gatewayService = binder.getService()
|
||||
bound = true
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
val localBinder = binder as ScaleGatewayService.LocalBinder
|
||||
scaleService = localBinder.getService()
|
||||
serviceBound = true
|
||||
scaleService?.statusCallback = { status ->
|
||||
runOnUiThread { updateScaleStatusUI(status) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
gatewayService = null
|
||||
bound = false
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
scaleService = null
|
||||
serviceBound = false
|
||||
}
|
||||
}
|
||||
|
||||
// Permission request launcher
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { results ->
|
||||
val allGranted = results.all { it.value }
|
||||
if (allGranted) {
|
||||
startGatewayService()
|
||||
} else {
|
||||
Toast.makeText(this, "BLE permissions required for scale gateway", Toast.LENGTH_LONG).show()
|
||||
// Start anyway without BLE
|
||||
startGatewayService()
|
||||
}
|
||||
// File chooser
|
||||
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
|
||||
|
||||
companion object {
|
||||
private const val BLE_PERMISSION_REQUEST = 1001
|
||||
private const val FILE_CHOOSER_REQUEST = 1002
|
||||
private const val PREFS_NAME = "evershelf_kiosk"
|
||||
private const val KEY_URL = "evershelf_url"
|
||||
private const val KEY_SETUP_COMPLETE = "setup_complete"
|
||||
private const val KEY_LAST_DEVICE = "last_device_address"
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityKioskBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setContentView(R.layout.activity_kiosk)
|
||||
|
||||
// Full-screen immersive mode
|
||||
enterKioskMode()
|
||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
bindViews()
|
||||
enterImmersiveMode()
|
||||
|
||||
// Keep screen on
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
if (prefs.getBoolean(KEY_SETUP_COMPLETE, false)) {
|
||||
launchWebView()
|
||||
} else {
|
||||
showWizard()
|
||||
}
|
||||
}
|
||||
|
||||
// Setup WebView
|
||||
setupWebView()
|
||||
private fun bindViews() {
|
||||
wizardContainer = findViewById(R.id.wizardContainer)
|
||||
webView = findViewById(R.id.webView)
|
||||
btnSettings = findViewById(R.id.btnSettings)
|
||||
step1 = findViewById(R.id.step1)
|
||||
step2 = findViewById(R.id.step2)
|
||||
step3 = findViewById(R.id.step3)
|
||||
stepIndicator = findViewById(R.id.stepIndicator)
|
||||
wizardUrl = findViewById(R.id.wizardUrl)
|
||||
urlStatus = findViewById(R.id.urlStatus)
|
||||
scaleStatusIcon = findViewById(R.id.scaleStatusIcon)
|
||||
scaleStatusText = findViewById(R.id.scaleStatusText)
|
||||
scaleStatusDetail = findViewById(R.id.scaleStatusDetail)
|
||||
|
||||
// Settings button (long press corner area)
|
||||
binding.btnSettings.setOnClickListener {
|
||||
// Step 1 buttons
|
||||
findViewById<MaterialButton>(R.id.btnGetStarted).setOnClickListener {
|
||||
goToStep(2)
|
||||
}
|
||||
|
||||
// Step 2 buttons
|
||||
findViewById<MaterialButton>(R.id.btnTestUrl).setOnClickListener {
|
||||
testConnection()
|
||||
}
|
||||
findViewById<MaterialButton>(R.id.btnStep2Back).setOnClickListener {
|
||||
goToStep(1)
|
||||
}
|
||||
findViewById<MaterialButton>(R.id.btnStep2Next).setOnClickListener {
|
||||
val url = wizardUrl.text.toString().trim()
|
||||
if (url.isEmpty()) {
|
||||
showUrlStatus("Please enter a URL", false)
|
||||
return@setOnClickListener
|
||||
}
|
||||
prefs.edit().putString(KEY_URL, url).apply()
|
||||
requestBlePermissions()
|
||||
goToStep(3)
|
||||
}
|
||||
|
||||
// Step 3 buttons
|
||||
findViewById<MaterialButton>(R.id.btnStep3Back).setOnClickListener {
|
||||
goToStep(2)
|
||||
}
|
||||
findViewById<MaterialButton>(R.id.btnFinish).setOnClickListener {
|
||||
finishWizard()
|
||||
}
|
||||
findViewById<MaterialButton>(R.id.btnSkipScale).setOnClickListener {
|
||||
finishWizard()
|
||||
}
|
||||
|
||||
// Settings button
|
||||
btnSettings.setOnClickListener {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
// Request permissions and start gateway
|
||||
requestPermissionsAndStart()
|
||||
|
||||
// Load the EverShelf URL
|
||||
loadEverShelfUrl()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
enterKioskMode()
|
||||
// Reload URL in case it was changed in settings
|
||||
val currentUrl = binding.webView.url ?: ""
|
||||
val savedUrl = getSavedUrl()
|
||||
if (currentUrl.isNotEmpty() && !currentUrl.startsWith(savedUrl)) {
|
||||
loadEverShelfUrl()
|
||||
// Pre-fill URL if saved
|
||||
val savedUrl = prefs.getString(KEY_URL, "") ?: ""
|
||||
if (savedUrl.isNotEmpty()) {
|
||||
wizardUrl.setText(savedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (bound) {
|
||||
unbindService(serviceConnection)
|
||||
bound = false
|
||||
}
|
||||
super.onDestroy()
|
||||
// ── Wizard Flow ───────────────────────────────────────────────────────
|
||||
|
||||
private fun showWizard() {
|
||||
wizardContainer.visibility = View.VISIBLE
|
||||
webView.visibility = View.GONE
|
||||
btnSettings.visibility = View.GONE
|
||||
goToStep(1)
|
||||
}
|
||||
|
||||
private fun enterKioskMode() {
|
||||
private fun goToStep(step: Int) {
|
||||
currentStep = step
|
||||
step1.visibility = if (step == 1) View.VISIBLE else View.GONE
|
||||
step2.visibility = if (step == 2) View.VISIBLE else View.GONE
|
||||
step3.visibility = if (step == 3) View.VISIBLE else View.GONE
|
||||
updateStepIndicator()
|
||||
|
||||
if (step == 3) {
|
||||
startScaleGateway()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStepIndicator() {
|
||||
stepIndicator.removeAllViews()
|
||||
for (i in 1..3) {
|
||||
val dot = View(this)
|
||||
val size = if (i == currentStep) 10 else 8
|
||||
val dp = (size * resources.displayMetrics.density).toInt()
|
||||
val params = LinearLayout.LayoutParams(dp, dp)
|
||||
params.marginStart = (4 * resources.displayMetrics.density).toInt()
|
||||
params.marginEnd = (4 * resources.displayMetrics.density).toInt()
|
||||
dot.layoutParams = params
|
||||
|
||||
val bg = GradientDrawable()
|
||||
bg.shape = GradientDrawable.OVAL
|
||||
if (i == currentStep) {
|
||||
bg.setColor(0xFF7c3aed.toInt())
|
||||
} else if (i < currentStep) {
|
||||
bg.setColor(0xFF34d399.toInt())
|
||||
} else {
|
||||
bg.setColor(0xFF334155.toInt())
|
||||
}
|
||||
dot.background = bg
|
||||
stepIndicator.addView(dot)
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishWizard() {
|
||||
prefs.edit().putBoolean(KEY_SETUP_COMPLETE, true).apply()
|
||||
wizardContainer.visibility = View.GONE
|
||||
launchWebView()
|
||||
}
|
||||
|
||||
fun resetWizard() {
|
||||
prefs.edit().putBoolean(KEY_SETUP_COMPLETE, false).apply()
|
||||
wizardContainer.visibility = View.VISIBLE
|
||||
webView.visibility = View.GONE
|
||||
btnSettings.visibility = View.GONE
|
||||
goToStep(1)
|
||||
}
|
||||
|
||||
// ── Connection Test ───────────────────────────────────────────────────
|
||||
|
||||
private fun testConnection() {
|
||||
val url = wizardUrl.text.toString().trim()
|
||||
if (url.isEmpty()) {
|
||||
showUrlStatus("Please enter a URL first", false)
|
||||
return
|
||||
}
|
||||
showUrlStatus("Testing connection...", null)
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val testUrl = if (url.endsWith("/")) "${url}api/" else "${url}/api/"
|
||||
val conn = URL(testUrl).openConnection() as HttpURLConnection
|
||||
conn.connectTimeout = 5000
|
||||
conn.readTimeout = 5000
|
||||
conn.requestMethod = "GET"
|
||||
val code = conn.responseCode
|
||||
conn.disconnect()
|
||||
runOnUiThread {
|
||||
if (code in 200..299) {
|
||||
showUrlStatus("✓ Connected successfully!", true)
|
||||
} else {
|
||||
showUrlStatus("⚠ Server responded with code $code", false)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
showUrlStatus("✗ Cannot reach server: ${e.message}", false)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun showUrlStatus(text: String, success: Boolean?) {
|
||||
urlStatus.visibility = View.VISIBLE
|
||||
urlStatus.text = text
|
||||
urlStatus.setTextColor(
|
||||
when (success) {
|
||||
true -> 0xFF34d399.toInt()
|
||||
false -> 0xFFf87171.toInt()
|
||||
null -> 0xFF94a3b8.toInt()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ── Scale Gateway ─────────────────────────────────────────────────────
|
||||
|
||||
private fun startScaleGateway() {
|
||||
val intent = Intent(this, ScaleGatewayService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
private fun updateScaleStatusUI(status: String) {
|
||||
when {
|
||||
status.contains("Connected", ignoreCase = true) -> {
|
||||
scaleStatusIcon.text = "✅"
|
||||
scaleStatusText.text = "Scale connected!"
|
||||
scaleStatusDetail.text = status
|
||||
scaleStatusDetail.setTextColor(0xFF34d399.toInt())
|
||||
}
|
||||
status.contains("Scanning", ignoreCase = true) ||
|
||||
status.contains("search", ignoreCase = true) -> {
|
||||
scaleStatusIcon.text = "🔍"
|
||||
scaleStatusText.text = "Scanning for scales..."
|
||||
scaleStatusDetail.text = "Turn on your scale and place it nearby"
|
||||
scaleStatusDetail.setTextColor(0xFF64748b.toInt())
|
||||
}
|
||||
status.contains("Ready", ignoreCase = true) ||
|
||||
status.contains("running", ignoreCase = true) -> {
|
||||
scaleStatusIcon.text = "📡"
|
||||
scaleStatusText.text = "Gateway is running"
|
||||
scaleStatusDetail.text = status
|
||||
scaleStatusDetail.setTextColor(0xFF94a3b8.toInt())
|
||||
}
|
||||
else -> {
|
||||
scaleStatusIcon.text = "📡"
|
||||
scaleStatusText.text = "Gateway active"
|
||||
scaleStatusDetail.text = status
|
||||
scaleStatusDetail.setTextColor(0xFF94a3b8.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── BLE Permissions ───────────────────────────────────────────────────
|
||||
|
||||
private fun requestBlePermissions() {
|
||||
val perms = mutableListOf<String>()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
|
||||
perms.add(Manifest.permission.BLUETOOTH_SCAN)
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED)
|
||||
perms.add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)
|
||||
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED)
|
||||
perms.add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
if (perms.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(this, perms.toTypedArray(), BLE_PERMISSION_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
// ── WebView ───────────────────────────────────────────────────────────
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun launchWebView() {
|
||||
webView.visibility = View.VISIBLE
|
||||
btnSettings.visibility = View.VISIBLE
|
||||
|
||||
val settings = webView.settings
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.mediaPlaybackRequiresUserGesture = false
|
||||
settings.allowFileAccess = true
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun onReceivedError(
|
||||
view: WebView?, request: WebResourceRequest?,
|
||||
error: WebResourceError?
|
||||
) {
|
||||
if (request?.isForMainFrame == true) {
|
||||
view?.loadData(errorPageHtml(), "text/html", "UTF-8")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onPermissionRequest(request: PermissionRequest?) {
|
||||
runOnUiThread { request?.grant(request.resources) }
|
||||
}
|
||||
override fun onConsoleMessage(msg: ConsoleMessage?): Boolean = true
|
||||
override fun onShowFileChooser(
|
||||
wv: WebView?,
|
||||
callback: ValueCallback<Array<Uri>>?,
|
||||
params: FileChooserParams?
|
||||
): Boolean {
|
||||
fileChooserCallback?.onReceiveValue(null)
|
||||
fileChooserCallback = callback
|
||||
val intent = params?.createIntent()
|
||||
if (intent != null) {
|
||||
startActivityForResult(intent, FILE_CHOOSER_REQUEST)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val url = prefs.getString(KEY_URL, "http://evershelf.local") ?: "http://evershelf.local"
|
||||
webView.loadUrl(url)
|
||||
|
||||
// Start scale gateway
|
||||
startScaleGateway()
|
||||
|
||||
// Keep screen on
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
private fun errorPageHtml(): String {
|
||||
val url = prefs.getString(KEY_URL, "") ?: ""
|
||||
return """
|
||||
<html>
|
||||
<head><meta name='viewport' content='width=device-width,initial-scale=1'></head>
|
||||
<body style='background:#0f172a;color:#f1f5f9;font-family:sans-serif;
|
||||
display:flex;flex-direction:column;align-items:center;
|
||||
justify-content:center;height:100vh;margin:0;padding:24px;
|
||||
text-align:center;'>
|
||||
<div style='font-size:48px;margin-bottom:16px;'>⚠️</div>
|
||||
<h2 style='margin:0 0 8px 0;'>Cannot reach EverShelf</h2>
|
||||
<p style='color:#94a3b8;margin:0 0 8px 0;'>$url</p>
|
||||
<p style='color:#64748b;font-size:14px;margin:0 0 32px 0;'>
|
||||
Check that the server is running and the URL is correct.
|
||||
</p>
|
||||
<button onclick='location.reload()'
|
||||
style='background:#7c3aed;color:#fff;border:none;padding:14px 32px;
|
||||
border-radius:12px;font-size:16px;cursor:pointer;'>
|
||||
Retry
|
||||
</button>
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
// ── Immersive Mode ────────────────────────────────────────────────────
|
||||
|
||||
private fun enterImmersiveMode() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.let {
|
||||
it.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||
it.hide(WindowInsets.Type.systemBars())
|
||||
it.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
@@ -122,120 +434,47 @@ class KioskActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebView() {
|
||||
binding.webView.apply {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.databaseEnabled = true
|
||||
settings.mediaPlaybackRequiresUserGesture = false
|
||||
settings.allowFileAccess = false
|
||||
settings.allowContentAccess = false
|
||||
settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
|
||||
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||
settings.useWideViewPort = true
|
||||
settings.loadWithOverviewMode = true
|
||||
settings.setSupportZoom(false)
|
||||
settings.builtInZoomControls = false
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
|
||||
// Allow camera access for barcode scanning
|
||||
webChromeClient = object : WebChromeClient() {
|
||||
override fun onPermissionRequest(request: PermissionRequest) {
|
||||
runOnUiThread {
|
||||
request.grant(request.resources)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
error: WebResourceError?
|
||||
) {
|
||||
// Show retry page on load error
|
||||
if (request?.isForMainFrame == true) {
|
||||
view?.loadData(
|
||||
"""
|
||||
<html><body style="font-family:sans-serif;text-align:center;padding:40px;background:#1a1a2e;color:#fff">
|
||||
<h2>⚠️ Connection Error</h2>
|
||||
<p>Cannot reach EverShelf server</p>
|
||||
<p style="color:#888;font-size:14px">${getSavedUrl()}</p>
|
||||
<button onclick="location.reload()" style="padding:12px 24px;font-size:16px;border:none;border-radius:8px;background:#7C3AED;color:#fff;cursor:pointer;margin-top:20px">Retry</button>
|
||||
<br><br>
|
||||
<button onclick="window.location='evershelf://settings'" style="padding:8px 16px;font-size:14px;border:1px solid #666;border-radius:8px;background:transparent;color:#aaa;cursor:pointer">Settings</button>
|
||||
</body></html>
|
||||
""".trimIndent(),
|
||||
"text/html", "utf-8"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
val url = request.url.toString()
|
||||
if (url.startsWith("evershelf://settings")) {
|
||||
startActivity(Intent(this@KioskActivity, SettingsActivity::class.java))
|
||||
return true
|
||||
}
|
||||
// Keep navigation within the WebView for same-origin
|
||||
return false
|
||||
}
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
enterImmersiveMode()
|
||||
// Reload WebView if setup is complete (in case URL changed in settings)
|
||||
if (prefs.getBoolean(KEY_SETUP_COMPLETE, false) && webView.visibility == View.VISIBLE) {
|
||||
val url = prefs.getString(KEY_URL, "") ?: ""
|
||||
if (url.isNotEmpty() && webView.url != url) {
|
||||
webView.loadUrl(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadEverShelfUrl() {
|
||||
val url = getSavedUrl()
|
||||
binding.webView.loadUrl(url)
|
||||
}
|
||||
|
||||
private fun getSavedUrl(): String {
|
||||
return getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||
.getString(PREF_URL, DEFAULT_URL) ?: DEFAULT_URL
|
||||
}
|
||||
|
||||
private fun requestPermissionsAndStart() {
|
||||
val needed = mutableListOf<String>()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN)
|
||||
!= PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.BLUETOOTH_SCAN)
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT)
|
||||
!= PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
if (needed.isNotEmpty()) {
|
||||
permissionLauncher.launch(needed.toTypedArray())
|
||||
} else {
|
||||
startGatewayService()
|
||||
// Check if wizard reset was requested
|
||||
if (!prefs.getBoolean(KEY_SETUP_COMPLETE, false) && wizardContainer.visibility != View.VISIBLE) {
|
||||
showWizard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startGatewayService() {
|
||||
val intent = Intent(this, ScaleGatewayService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == FILE_CHOOSER_REQUEST) {
|
||||
val result = if (resultCode == RESULT_OK && data != null) {
|
||||
WebChromeClient.FileChooserParams.parseResult(resultCode, data)
|
||||
} else null
|
||||
fileChooserCallback?.onReceiveValue(result)
|
||||
fileChooserCallback = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (serviceBound) {
|
||||
unbindService(serviceConnection)
|
||||
serviceBound = false
|
||||
}
|
||||
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
if (binding.webView.canGoBack()) {
|
||||
binding.webView.goBack()
|
||||
if (webView.visibility == View.VISIBLE && webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
}
|
||||
// Don't call super — prevent exiting kiosk mode
|
||||
// Block back button in kiosk mode
|
||||
}
|
||||
}
|
||||
|
||||
+80
-18
@@ -1,39 +1,101 @@
|
||||
package it.dadaloop.evershelf.kiosk
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import it.dadaloop.evershelf.kiosk.databinding.ActivitySettingsBinding
|
||||
|
||||
private const val PREFS_NAME = "evershelf_kiosk"
|
||||
private const val PREF_URL = "evershelf_url"
|
||||
private const val DEFAULT_URL = "http://evershelf.local"
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private lateinit var urlEdit: EditText
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "evershelf_kiosk"
|
||||
private const val KEY_URL = "evershelf_url"
|
||||
private const val KEY_SETUP_COMPLETE = "setup_complete"
|
||||
private const val KEY_LAST_DEVICE = "last_device_address"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setContentView(R.layout.activity_settings)
|
||||
|
||||
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||
binding.editUrl.setText(prefs.getString(PREF_URL, DEFAULT_URL))
|
||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
urlEdit = findViewById(R.id.urlEdit)
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
val url = binding.editUrl.text.toString().trim()
|
||||
// Load saved URL
|
||||
urlEdit.setText(prefs.getString(KEY_URL, "") ?: "")
|
||||
|
||||
// Scale status
|
||||
val scaleDevice = prefs.getString(KEY_LAST_DEVICE, null)
|
||||
findViewById<TextView>(R.id.scaleDeviceInfo).text =
|
||||
if (scaleDevice != null) "Last connected: $scaleDevice" else "No scale connected yet"
|
||||
|
||||
// Back button
|
||||
findViewById<android.widget.ImageButton>(R.id.btnBack).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
// Test connection
|
||||
findViewById<MaterialButton>(R.id.btnTestConnection).setOnClickListener {
|
||||
testConnection()
|
||||
}
|
||||
|
||||
// Run wizard again
|
||||
findViewById<MaterialButton>(R.id.btnRunWizard).setOnClickListener {
|
||||
prefs.edit().putBoolean(KEY_SETUP_COMPLETE, false).apply()
|
||||
Toast.makeText(this, "Wizard will run on next launch", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
// Save
|
||||
findViewById<MaterialButton>(R.id.btnSave).setOnClickListener {
|
||||
val url = urlEdit.text.toString().trim()
|
||||
if (url.isEmpty()) {
|
||||
Toast.makeText(this, "URL cannot be empty", Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
prefs.edit().putString(PREF_URL, url).apply()
|
||||
Toast.makeText(this, "Saved! Returning to kiosk...", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.btnBack.setOnClickListener {
|
||||
prefs.edit().putString(KEY_URL, url).apply()
|
||||
Toast.makeText(this, "Settings saved", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun testConnection() {
|
||||
val url = urlEdit.text.toString().trim()
|
||||
if (url.isEmpty()) {
|
||||
Toast.makeText(this, "Enter a URL first", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val testUrl = if (url.endsWith("/")) "${url}api/" else "${url}/api/"
|
||||
val conn = URL(testUrl).openConnection() as HttpURLConnection
|
||||
conn.connectTimeout = 5000
|
||||
conn.readTimeout = 5000
|
||||
conn.requestMethod = "GET"
|
||||
val code = conn.responseCode
|
||||
conn.disconnect()
|
||||
runOnUiThread {
|
||||
if (code in 200..299) {
|
||||
Toast.makeText(this, "✓ Connection successful!", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, "⚠ Server responded: $code", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "✗ Cannot reach server", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#1e293b" />
|
||||
<corners android:radius="16dp" />
|
||||
<stroke android:width="1dp" android:color="#334155" />
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#1e293b" />
|
||||
<corners android:radius="12dp" />
|
||||
<stroke android:width="1dp" android:color="#334155" />
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#0f1729" />
|
||||
<corners android:radius="10dp" />
|
||||
<stroke android:width="1dp" android:color="#1e293b" />
|
||||
</shape>
|
||||
@@ -2,25 +2,412 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#000000">
|
||||
android:background="#0f172a">
|
||||
|
||||
<!-- Setup wizard container (shown on first launch) -->
|
||||
<ScrollView
|
||||
android:id="@+id/wizardContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:paddingTop="48dp"
|
||||
android:paddingBottom="48dp">
|
||||
|
||||
<!-- Step indicator -->
|
||||
<LinearLayout
|
||||
android:id="@+id/stepIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="40dp"
|
||||
android:gravity="center" />
|
||||
|
||||
<!-- Step 1: Welcome -->
|
||||
<LinearLayout
|
||||
android:id="@+id/step1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🏠"
|
||||
android:textSize="64sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Welcome to\nEverShelf Kiosk"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Turn this tablet into a dedicated kitchen panel for your smart pantry. Always-on display with built-in smart scale support."
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="6dp"
|
||||
android:layout_marginBottom="48dp" />
|
||||
|
||||
<!-- Feature highlights -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="48dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📺 Full-screen kiosk mode"
|
||||
android:textColor="#cbd5e1"
|
||||
android:textSize="15sp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="⚖️ Built-in BLE scale gateway"
|
||||
android:textColor="#cbd5e1"
|
||||
android:textSize="15sp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🔋 Always-on, screen never sleeps"
|
||||
android:textColor="#cbd5e1"
|
||||
android:textSize="15sp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📷 Barcode scanning from WebView"
|
||||
android:textColor="#cbd5e1"
|
||||
android:textSize="15sp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnGetStarted"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:text="Get Started"
|
||||
android:textSize="17sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#7c3aed" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Step 2: Server URL -->
|
||||
<LinearLayout
|
||||
android:id="@+id/step2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🌐"
|
||||
android:textSize="48sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Connect to EverShelf"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Enter the URL of your EverShelf server. This is the address you use to access EverShelf from a browser."
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="15sp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:layout_marginBottom="32dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Server URL"
|
||||
android:textColor="#cbd5e1"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/wizardUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="https://192.168.1.100/dispensa"
|
||||
android:inputType="textUri"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textColorHint="#475569"
|
||||
android:background="@drawable/input_background"
|
||||
android:padding="16dp"
|
||||
android:textSize="16sp"
|
||||
android:singleLine="true"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/urlStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="13sp"
|
||||
android:visibility="gone"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnTestUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="🔗 Test Connection"
|
||||
android:textSize="15sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#334155"
|
||||
android:textColor="#94a3b8"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="💡 Tip: You can find this URL in your browser's address bar when EverShelf is open."
|
||||
android:textColor="#64748b"
|
||||
android:textSize="13sp"
|
||||
android:lineSpacingExtra="2dp"
|
||||
android:background="@drawable/tip_background"
|
||||
android:padding="14dp"
|
||||
android:layout_marginBottom="32dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnStep2Back"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Back"
|
||||
android:textSize="15sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#334155"
|
||||
android:textColor="#94a3b8"
|
||||
android:layout_marginEnd="12dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnStep2Next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="2"
|
||||
android:text="Next →"
|
||||
android:textSize="15sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#7c3aed" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Step 3: Scale Setup -->
|
||||
<LinearLayout
|
||||
android:id="@+id/step3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="⚖️"
|
||||
android:textSize="48sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Smart Scale (Optional)"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="This app includes a built-in BLE scale gateway. If you have a Bluetooth kitchen scale, it will be detected automatically."
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="15sp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<!-- Scale status card -->
|
||||
<LinearLayout
|
||||
android:id="@+id/scaleStatusCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="20dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scaleStatusIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🔍"
|
||||
android:textSize="32sp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scaleStatusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Searching for scales..."
|
||||
android:textColor="#cbd5e1"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scaleStatusDetail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Turn on your scale and place it nearby"
|
||||
android:textColor="#64748b"
|
||||
android:textSize="13sp"
|
||||
android:gravity="center" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Gateway info -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/tip_background"
|
||||
android:padding="14dp"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="ℹ️ Gateway runs on ws://localhost:8765"
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="13sp"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="EverShelf will auto-configure the scale connection when running in kiosk mode."
|
||||
android:textColor="#64748b"
|
||||
android:textSize="12sp"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnStep3Back"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Back"
|
||||
android:textSize="15sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#334155"
|
||||
android:textColor="#94a3b8"
|
||||
android:layout_marginEnd="12dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnFinish"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="2"
|
||||
android:text="🚀 Launch EverShelf"
|
||||
android:textSize="16sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#059669" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSkipScale"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:text="Skip — I don't have a scale"
|
||||
android:textSize="13sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:textColor="#64748b"
|
||||
android:layout_marginTop="12dp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- WebView (shown after setup) -->
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Settings button — small transparent gear icon in top-right corner -->
|
||||
<!-- Settings gear (shown after setup, over WebView) -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnSettings"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@android:drawable/ic_menu_manage"
|
||||
android:alpha="0.15"
|
||||
android:alpha="0.12"
|
||||
android:contentDescription="Settings"
|
||||
android:scaleType="centerInside" />
|
||||
android:scaleType="centerInside"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -1,69 +1,195 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="#1a1a2e"
|
||||
android:padding="32dp"
|
||||
android:gravity="center">
|
||||
android:background="#0f172a"
|
||||
android:fillViewport="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="⚙️ EverShelf Kiosk Settings"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="32dp" />
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="EverShelf Server URL"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="http://192.168.1.100"
|
||||
android:inputType="textUri"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textColorHint="#666666"
|
||||
android:background="#2a2a3e"
|
||||
android:padding="14dp"
|
||||
android:textSize="16sp"
|
||||
android:singleLine="true"
|
||||
android:layout_marginBottom="24dp" />
|
||||
<!-- Header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="The app will display this URL in full-screen kiosk mode.\nThe Scale Gateway runs on port 8765 (WebSocket).\nSet the gateway URL in EverShelf settings to:\nws://localhost:8765"
|
||||
android:textColor="#888888"
|
||||
android:textSize="13sp"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:layout_marginBottom="32dp" />
|
||||
<ImageButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/card_background"
|
||||
android:src="@android:drawable/ic_menu_revert"
|
||||
android:scaleType="centerInside"
|
||||
android:contentDescription="Back"
|
||||
android:layout_marginEnd="16dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Save & Return"
|
||||
android:textSize="16sp"
|
||||
android:backgroundTint="#7C3AED"
|
||||
android:layout_marginBottom="12dp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Settings"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Cancel"
|
||||
android:textSize="14sp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#666666"
|
||||
android:textColor="#AAAAAA" />
|
||||
<!-- Server URL Section -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="SERVER CONNECTION"
|
||||
android:textColor="#7c3aed"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.1"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="16dp"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="EverShelf URL"
|
||||
android:textColor="#cbd5e1"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/urlEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:textColor="#f1f5f9"
|
||||
android:textColorHint="#475569"
|
||||
android:hint="https://192.168.1.100/dispensa"
|
||||
android:background="@drawable/input_background"
|
||||
android:padding="14dp"
|
||||
android:textSize="15sp"
|
||||
android:singleLine="true"
|
||||
android:layout_marginBottom="10dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnTestConnection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:text="Test Connection"
|
||||
android:textSize="13sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#334155"
|
||||
android:textColor="#94a3b8" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Scale Section -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="SMART SCALE"
|
||||
android:textColor="#7c3aed"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.1"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="16dp"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Scale Gateway"
|
||||
android:textColor="#cbd5e1"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scaleGatewayStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Active"
|
||||
android:textColor="#34d399"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scaleDeviceInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="No scale connected"
|
||||
android:textColor="#64748b"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="RESET"
|
||||
android:textColor="#ef4444"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:letterSpacing="0.1"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/card_background"
|
||||
android:padding="16dp"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnRunWizard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp"
|
||||
android:text="Run Setup Wizard Again"
|
||||
android:textSize="14sp"
|
||||
android:textAllCaps="false"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:strokeColor="#334155"
|
||||
android:textColor="#94a3b8" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Spacer -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!-- Buttons -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:text="Save Changes"
|
||||
android:textSize="16sp"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="#7c3aed"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="accent">#7C3AED</color>
|
||||
<color name="green">#059669</color>
|
||||
<color name="red">#EF4444</color>
|
||||
<color name="blue">#1D4ED8</color>
|
||||
<color name="ic_launcher_background">#10B981</color>
|
||||
<color name="bg_dark">#0f172a</color>
|
||||
<color name="bg_card">#1e293b</color>
|
||||
<color name="text_primary">#f1f5f9</color>
|
||||
<color name="text_secondary">#94a3b8</color>
|
||||
<color name="text_muted">#64748b</color>
|
||||
<color name="accent_purple">#7c3aed</color>
|
||||
<color name="accent_green">#059669</color>
|
||||
<color name="border">#334155</color>
|
||||
<color name="success">#34d399</color>
|
||||
<color name="error">#f87171</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EverShelf Kiosk</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user