summaryrefslogtreecommitdiff
path: root/android/app/src/main/java/org/terst/doot/widget/ui/DashboardActivity.kt
blob: c7cdfaa0f173f70b4b95571642d5b9e71562b972 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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<String?>(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 <title>Personal Dashboard</title> 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)
    }
}