summaryrefslogtreecommitdiff
path: root/android/app/src/main/java/org/terst
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-06 16:57:44 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-06 16:57:44 +0000
commita1ca48e0dc760393a59fe745736b6b0668fade17 (patch)
tree5313abafd45e05218dc2c098919349138dc4f529 /android/app/src/main/java/org/terst
parentb56da63fe7776b14bc2cff4fe3c63ae2dd65ac73 (diff)
Widget: give Dashboard its own launcher icon, add toolbar nav
DashboardActivity is now the app's single home-screen launcher entry (MAIN/LAUNCHER intent-filter), replacing SettingsActivity in that role. SettingsActivity keeps only APPWIDGET_CONFIGURE (still exported=true, since the launcher/home-screen process starts it directly during widget placement) and is now reached via a gear button in Dashboard's toolbar. DashboardActivity itself is rewritten from a bare setContentView(webView) to Compose Scaffold/TopAppBar wrapping the WebView via AndroidView, adding: - a back arrow (shown only when the WebView has history) instead of relying solely on the hardware back key - a settings gear action launching SettingsActivity - the page title tracked from the loaded page Also fixes a race in the original version: server URL was loaded via a lifecycleScope coroutine racing the WebView's own initialization order. Now it's plain Compose state (LaunchedEffect + AndroidView's update callback), so the WebView never loads before the URL is known. No native tab bar: web/templates/index.html's tabs are HTMX partials (hx-get targeting #tab-content), not separate pages, so loading "/" in the WebView already gets full in-app navigation for free. Verified on emulator-5556 (doot_test_api36): pm resolve-activity confirms DashboardActivity is the launcher default; launched it, confirmed no crash and topResumedActivity is Dashboard; configured a dummy server URL via Settings and confirmed the toolbar renders (title, gear icon) and the gear button correctly navigates to SettingsActivity (topResumedActivity becomes SettingsActivity) with no crash. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'android/app/src/main/java/org/terst')
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/ui/DashboardActivity.kt158
1 files changed, 116 insertions, 42 deletions
diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/DashboardActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/DashboardActivity.kt
index f5ae744..159b416 100644
--- a/android/app/src/main/java/org/terst/doot/widget/ui/DashboardActivity.kt
+++ b/android/app/src/main/java/org/terst/doot/widget/ui/DashboardActivity.kt
@@ -1,74 +1,148 @@
package org.terst.doot.widget.ui
import android.content.Intent
-import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.net.toUri
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-import androidx.lifecycle.lifecycleScope
import org.terst.doot.widget.data.Keys
import org.terst.doot.widget.data.dataStore
/**
- * Wraps the existing web dashboard in a plain WebView -- the same session-cookie
- * login flow a browser would use (see internal/auth/middleware.go's RequireAuth),
- * no separate auth bridge needed since the widget's own bearer token is a
- * completely different scheme. First cut per the 2026-08-06 feasibility check:
- * one Activity, no deep-linking to specific tabs, no native-rendered chrome --
- * just prove the wrapped experience is worth building further before investing
- * in anything past this.
+ * Wraps the existing web dashboard in a WebView -- the same session-cookie
+ * login flow a browser would use (see internal/auth/middleware.go's
+ * RequireAuth), no separate auth bridge needed since the widget's own bearer
+ * token is a completely different scheme.
+ *
+ * No native tab bar: the dashboard's tabs (Tasks/Planning/Meals/...) are
+ * HTMX partials (hx-get="/tabs/..." targeting #tab-content in
+ * web/templates/index.html), swapped into ONE page client-side, not separate
+ * full pages -- navigating a WebView directly to e.g. /tabs/timeline would
+ * show a bare unstyled fragment. Loading "/" already gets the real
+ * in-page nav for free; what a bare WebView is actually missing is
+ * browser-chrome-level navigation (no back arrow, no way back to Settings),
+ * which is what the top bar below adds.
*/
class DashboardActivity : ComponentActivity() {
- private lateinit var webView: WebView
+ private var webViewRef: WebView? = null
+ private var serverHost: String? = null
+ @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- webView = WebView(this).apply {
- settings.javaScriptEnabled = true
- settings.domStorageEnabled = true
- settings.setSupportZoom(false)
- webViewClient = object : WebViewClient() {
- override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
- val uri = url.toUri()
- val sameHost = uri.host == null || uri.host == serverHost
- if (sameHost) return false
- // External links (e.g. a calendar event's source URL) open in the
- // user's actual browser/app instead of getting stuck in this WebView.
- startActivity(Intent(Intent.ACTION_VIEW, uri))
- return true
- }
- }
- }
- setContentView(webView)
-
CookieManager.getInstance().setAcceptCookie(true)
- CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
- lifecycleScope.launch {
- val prefs = dataStore.data.first()
- val url = prefs[Keys.SERVER_URL]?.trimEnd('/')
- if (url.isNullOrBlank()) {
- finish()
- return@launch
+ setContent {
+ MaterialTheme(colorScheme = darkColorScheme()) {
+ var serverUrl by remember { mutableStateOf<String?>(null) }
+ var canGoBack by remember { mutableStateOf(false) }
+ var pageTitle by remember { mutableStateOf("Doot") }
+ val context = LocalContext.current
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(pageTitle) },
+ navigationIcon = {
+ if (canGoBack) {
+ IconButton(onClick = { webViewRef?.goBack() }) {
+ // Plain text glyphs, not Compose Material Icons --
+ // matches this codebase's existing convention (see
+ // WidgetRows.kt: every other icon here is a
+ // hand-authored R.drawable vector, zero Icons.* usage
+ // anywhere), rather than introducing that dependency
+ // for two buttons.
+ Text("←", style = MaterialTheme.typography.titleLarge)
+ }
+ }
+ },
+ actions = {
+ IconButton(onClick = {
+ context.startActivity(Intent(context, SettingsActivity::class.java))
+ }) {
+ Text("⚙", style = MaterialTheme.typography.titleLarge)
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ AndroidView(
+ modifier = Modifier.padding(innerPadding),
+ factory = { ctx ->
+ WebView(ctx).apply {
+ webViewRef = this
+ settings.javaScriptEnabled = true
+ settings.domStorageEnabled = true
+ settings.setSupportZoom(false)
+ CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)
+ webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
+ val uri = url.toUri()
+ val sameHost = uri.host == null || uri.host == serverHost
+ if (sameHost) return false
+ // External links (e.g. a calendar event's source URL)
+ // open in the user's actual browser instead of getting
+ // stuck in this WebView.
+ context.startActivity(Intent(Intent.ACTION_VIEW, uri))
+ return true
+ }
+
+ override fun onPageFinished(view: WebView, url: String?) {
+ canGoBack = view.canGoBack()
+ pageTitle = view.title?.takeIf { it.isNotBlank() } ?: "Doot"
+ }
+ }
+ }
+ },
+ update = { webView ->
+ val url = serverUrl
+ if (url != null && webView.url == null) {
+ webView.loadUrl(url)
+ }
+ }
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ val prefs = dataStore.data.first()
+ val url = prefs[Keys.SERVER_URL]?.trimEnd('/')
+ if (url.isNullOrBlank()) {
+ finish()
+ } else {
+ serverHost = url.toUri().host
+ serverUrl = url
+ }
+ }
}
- serverHost = url.toUri().host
- webView.loadUrl(url)
}
}
- private var serverHost: String? = null
-
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
- if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
- webView.goBack()
+ if (keyCode == KeyEvent.KEYCODE_BACK && webViewRef?.canGoBack() == true) {
+ webViewRef?.goBack()
return true
}
return super.onKeyDown(keyCode, event)