package org.terst.doot.widget.ui import android.content.Intent 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 org.terst.doot.widget.data.Keys import org.terst.doot.widget.data.dataStore /** * 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 var webViewRef: WebView? = null private var serverHost: String? = null @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) CookieManager.getInstance().setAcceptCookie(true) setContent { MaterialTheme(colorScheme = darkColorScheme()) { var serverUrl by remember { mutableStateOf(null) } var canGoBack by remember { mutableStateOf(false) } val context = LocalContext.current Scaffold( topBar = { TopAppBar( // Static, not tracking the WebView's document.title: the // dashboard is a single HTMX-swapped page (see class doc), // so title never meaningfully changes, and showing the // page's own Personal Dashboard here just // reads as two different app names stacked on top of // each other. title = { Text("doot") }, 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() } } } }, 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 } } } } } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { if (keyCode == KeyEvent.KEYCODE_BACK && webViewRef?.canGoBack() == true) { webViewRef?.goBack() return true } return super.onKeyDown(keyCode, event) } }