diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 9facf7371..f6f88adc3 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -32,7 +32,7 @@ import com.pitchedapps.frost.main.MainActivity import com.pitchedapps.frost.web.state.FrostWebStore import com.pitchedapps.frost.web.state.TabAction import com.pitchedapps.frost.web.state.TabListAction -import com.pitchedapps.frost.web.state.TabWebState +import com.pitchedapps.frost.web.state.state.HomeTabSessionState import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -72,7 +72,7 @@ class StartActivity : AppCompatActivity() { // Test something scrollable store.dispatch( TabAction( - tabId = TabWebState.homeTabId(0), + tabId = HomeTabSessionState.homeTabId(0), TabAction.ContentAction.UpdateUrlAction( "https://github.com/AllanWang/Frost-for-Facebook" ), @@ -80,7 +80,7 @@ class StartActivity : AppCompatActivity() { ) store.dispatch( TabAction( - tabId = TabWebState.homeTabId(1), + tabId = HomeTabSessionState.homeTabId(1), TabAction.ContentAction.UpdateUrlAction("https://github.com/AllanWang/KAU"), ), ) diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/webview/FrostWebCompose.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/webview/FrostWebCompose.kt index c4e95a841..cb24703c1 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/webview/FrostWebCompose.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/webview/FrostWebCompose.kt @@ -37,8 +37,8 @@ import com.pitchedapps.frost.web.state.FrostWebStore import com.pitchedapps.frost.web.state.TabAction import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction import com.pitchedapps.frost.web.state.TabAction.ResponseAction.WebStepResponseAction -import com.pitchedapps.frost.web.state.TabWebState import com.pitchedapps.frost.web.state.get +import com.pitchedapps.frost.web.state.state.ContentState import com.pitchedapps.frost.webview.FrostChromeClient import com.pitchedapps.frost.webview.FrostWebViewClient import kotlinx.coroutines.flow.Flow @@ -88,13 +88,14 @@ class FrostWebCompose( webView?.let { wv -> val lifecycleOwner = LocalLifecycleOwner.current - val canGoBack by store.observeAsState(initialValue = false) { it[tabId]?.canGoBack == true } + val canGoBack by + store.observeAsState(initialValue = false) { it[tabId]?.content?.canGoBack == true } BackHandler(captureBackPresses && canGoBack) { wv.goBack() } LaunchedEffect(wv, store) { - fun storeFlow(action: suspend Flow.() -> Unit) = launch { - store.flow(lifecycleOwner).mapNotNull { it[tabId] }.action() + fun storeFlow(action: suspend Flow.() -> Unit) = launch { + store.flow(lifecycleOwner).mapNotNull { it[tabId]?.content }.action() } storeFlow { @@ -128,6 +129,8 @@ class FrostWebCompose( .apply { onCreated(this) + logger.atInfo().log("Created webview for %s", tabId) + this.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, @@ -141,7 +144,7 @@ class FrostWebCompose( webChromeClient = chromeClient webViewClient = client - val url = store.state[tabId]?.url + val url = store.state[tabId]?.content?.url if (url != null) loadUrl(url) } .also { webView = it } @@ -163,6 +166,7 @@ class FrostWebCompose( onRelease = { parentFrame -> val wv = parentFrame.children.first() as WebView onDispose(wv) + logger.atInfo().log("Released webview for %s", tabId) }, ) } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt index 75f577aca..28af0532d 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt @@ -39,6 +39,7 @@ import androidx.compose.material.icons.filled.Store import androidx.compose.material.icons.filled.Today import androidx.compose.ui.graphics.vector.ImageVector import com.pitchedapps.frost.R +import com.pitchedapps.frost.ext.WebTargetId import com.pitchedapps.frost.main.MainTabItem import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid @@ -118,8 +119,9 @@ enum class FbItem( } /** Converts [FbItem] to [MainTabItem]. */ -fun FbItem.tab(context: Context): MainTabItem = +fun FbItem.tab(context: Context, id: WebTargetId): MainTabItem = MainTabItem( + id = id, title = context.getString(titleId), icon = icon, url = url, diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/hilt/FrostWebViewModule.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/hilt/FrostWebViewModule.kt index 70a0143b7..e03b913b9 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/hilt/FrostWebViewModule.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/hilt/FrostWebViewModule.kt @@ -20,6 +20,7 @@ import android.webkit.CookieManager import com.google.common.flogger.FluentLogger import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.web.state.FrostLoggerMiddleware +import com.pitchedapps.frost.web.state.FrostWebReducer import com.pitchedapps.frost.web.state.FrostWebStore import dagger.Module import dagger.Provides @@ -38,11 +39,9 @@ object FrostWebViewModule { @Provides @Singleton - fun frostWebStore(): FrostWebStore { + fun frostWebStore(frostWebReducer: FrostWebReducer): FrostWebStore { val middleware = buildList { if (BuildConfig.DEBUG) add(FrostLoggerMiddleware()) } - val store = FrostWebStore(middleware = middleware) - - return store + return FrostWebStore(frostWebReducer = frostWebReducer, middleware = middleware) } } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainActivity.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainActivity.kt index 73fc7f523..23192ccfc 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainActivity.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainActivity.kt @@ -22,7 +22,10 @@ import androidx.activity.compose.setContent import androidx.core.view.WindowCompat import com.google.common.flogger.FluentLogger import com.pitchedapps.frost.compose.FrostTheme +import com.pitchedapps.frost.web.state.FrostWebStore import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import mozilla.components.lib.state.ext.observeAsState /** * Main activity. @@ -32,6 +35,8 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { + @Inject lateinit var store: FrostWebStore + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -43,7 +48,12 @@ class MainActivity : ComponentActivity() { // MainScreen( // tabs = tabs, // ) - MainScreenWebView() + + val tabs = + store.observeAsState(initialValue = null) { it.homeTabs.map { it.tab } }.value + ?: return@FrostTheme + + MainScreenWebView(homeTabs = tabs) } } } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainData.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainData.kt index e40f191e0..75aeb74db 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainData.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainData.kt @@ -17,6 +17,12 @@ package com.pitchedapps.frost.main import androidx.compose.ui.graphics.vector.ImageVector +import com.pitchedapps.frost.ext.WebTargetId /** Data representation of a single main tab entry. */ -data class MainTabItem(val title: String, val icon: ImageVector, val url: String) +data class MainTabItem( + val id: WebTargetId, + val title: String, + val icon: ImageVector, + val url: String +) diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt index 2b8164150..e2c2f7935 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt @@ -23,6 +23,9 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -35,37 +38,81 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel +import com.pitchedapps.frost.ext.WebTargetId import com.pitchedapps.frost.web.state.FrostWebStore +import com.pitchedapps.frost.web.state.TabListAction.SelectHomeTab import com.pitchedapps.frost.webview.FrostWebComposer import kotlinx.coroutines.delay import kotlinx.coroutines.launch import mozilla.components.lib.state.ext.observeAsState @Composable -fun MainScreenWebView(modifier: Modifier = Modifier) { +fun MainScreenWebView(modifier: Modifier = Modifier, homeTabs: List) { val vm: MainScreenViewModel = viewModel() + + val selectedHomeTab by vm.store.observeAsState(initialValue = null) { it.selectedHomeTab } + Scaffold( modifier = modifier, topBar = { MainTopBar(modifier = modifier) }, + bottomBar = { + MainBottomBar( + selectedTab = selectedHomeTab, + items = homeTabs, + onSelect = { vm.store.dispatch(SelectHomeTab(it)) }, + ) + }, ) { paddingValues -> MainScreenWebContainer( modifier = Modifier.padding(paddingValues), + selectedTab = selectedHomeTab, + items = homeTabs, store = vm.store, frostWebComposer = vm.frostWebComposer, ) } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainTopBar(modifier: Modifier = Modifier) { + TopAppBar(modifier = modifier, title = { Text(text = "Title") }) +} + +@Composable +fun MainBottomBar( + modifier: Modifier = Modifier, + selectedTab: WebTargetId?, + items: List, + onSelect: (WebTargetId) -> Unit +) { + NavigationBar(modifier = modifier) { + items.forEach { item -> + NavigationBarItem( + icon = { Icon(item.icon, contentDescription = item.title) }, + selected = selectedTab == item.id, + onClick = { onSelect(item.id) }, + ) + } + } +} + @Composable private fun MainScreenWebContainer( modifier: Modifier, + selectedTab: WebTargetId?, + items: List, store: FrostWebStore, frostWebComposer: FrostWebComposer ) { - val homeTabs by store.observeAsState(initialValue = emptyList()) { it.homeTabs } - val homeTabComposables = remember(homeTabs) { homeTabs.map { frostWebComposer.create(it.id) } } + val homeTabComposables = remember(items) { items.map { frostWebComposer.create(it.id) } } - PullRefresh(modifier = modifier, store = store) { homeTabComposables.firstOrNull()?.WebView() } + PullRefresh( + modifier = modifier, + store = store, + ) { + homeTabComposables.find { it.tabId == selectedTab }?.WebView() + } } @OptIn(ExperimentalMaterialApi::class) @@ -89,9 +136,3 @@ private fun PullRefresh(modifier: Modifier, store: FrostWebStore, content: @Comp PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) } } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MainTopBar(modifier: Modifier = Modifier) { - TopAppBar(title = { Text(text = "Title") }) -} diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebAction.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebAction.kt index f4d11bbd0..44af46de9 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebAction.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebAction.kt @@ -39,6 +39,8 @@ object InitAction : FrostWebAction /** Actions affecting multiple tabs */ sealed interface TabListAction : FrostWebAction { data class SetHomeTabs(val data: List) : TabListAction + + data class SelectHomeTab(val id: WebTargetId) : TabListAction } /** Action affecting a single tab */ diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebReducer.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebReducer.kt index 27f787831..9e2c429af 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebReducer.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebReducer.kt @@ -19,6 +19,10 @@ package com.pitchedapps.frost.web.state import com.pitchedapps.frost.ext.WebTargetId import com.pitchedapps.frost.web.state.reducer.ContentStateReducer import com.pitchedapps.frost.web.state.reducer.TabListReducer +import com.pitchedapps.frost.web.state.state.FloatingTabSessionState +import com.pitchedapps.frost.web.state.state.HomeTabSessionState +import com.pitchedapps.frost.web.state.state.SessionState +import javax.inject.Inject /** * See @@ -26,25 +30,35 @@ import com.pitchedapps.frost.web.state.reducer.TabListReducer * * For firefox example */ -internal object FrostWebReducer { +class FrostWebReducer +@Inject +internal constructor( + private val tabListReducer: TabListReducer, + private val contentStateReducer: ContentStateReducer +) { fun reduce(state: FrostWebState, action: FrostWebAction): FrostWebState { return when (action) { is InitAction -> state - is TabListAction -> TabListReducer.reduce(state, action) + is TabListAction -> tabListReducer.reduce(state, action) is TabAction -> - state.updateTabState(action.tabId) { ContentStateReducer.reduce(it, action.action) } + state.updateTabState(action.tabId) { session -> + val newContent = contentStateReducer.reduce(session.content, action.action) + session.createCopy(content = newContent) + } } } } +@Suppress("Unchecked_Cast") internal fun FrostWebState.updateTabState( tabId: WebTargetId, - update: (TabWebState) -> TabWebState, + update: (SessionState) -> SessionState, ): FrostWebState { val floatingTabMatch = floatingTab?.takeIf { it.id == tabId } - if (floatingTabMatch != null) return copy(floatingTab = update(floatingTabMatch)) + if (floatingTabMatch != null) + return copy(floatingTab = update(floatingTabMatch) as FloatingTabSessionState) - val newHomeTabs = homeTabs.updateTabs(tabId, update) + val newHomeTabs = homeTabs.updateTabs(tabId, update) as List? if (newHomeTabs != null) return copy(homeTabs = newHomeTabs) return this } @@ -55,12 +69,11 @@ internal fun FrostWebState.updateTabState( * @param tabId ID of the tab to change. * @param update Returns a new version of the tab state. */ -internal fun List.updateTabs( +internal fun List.updateTabs( tabId: WebTargetId, - update: (TabWebState) -> TabWebState, -): List? { + update: (T) -> T, +): List? { val tabIndex = indexOfFirst { it.id == tabId } if (tabIndex == -1) return null - return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size) } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebState.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebState.kt index b922a8241..3e621f7d1 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebState.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebState.kt @@ -16,9 +16,10 @@ */ package com.pitchedapps.frost.web.state -import androidx.compose.ui.graphics.vector.ImageVector import com.pitchedapps.frost.ext.FrostAccountId import com.pitchedapps.frost.ext.WebTargetId +import com.pitchedapps.frost.web.state.state.FloatingTabSessionState +import com.pitchedapps.frost.web.state.state.HomeTabSessionState import mozilla.components.lib.state.State /** @@ -29,8 +30,9 @@ import mozilla.components.lib.state.State */ data class FrostWebState( val auth: AuthWebState = AuthWebState(), - val homeTabs: List = emptyList(), - var floatingTab: TabWebState? = null, + val selectedHomeTab: WebTargetId? = null, + val homeTabs: List = emptyList(), + var floatingTab: FloatingTabSessionState? = null, ) : State /** @@ -57,40 +59,3 @@ data class AuthWebState( object Unknown : AuthUser } } - -data class TabWebState( - val id: WebTargetId, - val userId: AuthWebState.AuthUser, - val baseUrl: String, - val url: String, - val icon: ImageVector? = null, - val title: String? = null, - val progress: Int = 100, - val loading: Boolean = false, - val canGoBack: Boolean = false, - val canGoForward: Boolean = false, - val transientState: TransientWebState = TransientWebState(), -) { - companion object { - fun homeTabId(index: Int): WebTargetId = WebTargetId("home-tab--$index") - - val FLOATING_TAB_ID = WebTargetId("floating-tab") - } -} - -/** - * Transient web state. - * - * While we typically don't want to store this, our webview is not a composable, and requires a - * bridge to handle events. - * - * This state is not a list of pending actions, but rather a snapshot of the expected changes so - * that conflicting events can be ignored. - * - * @param targetUrl url destination if nonnull - * @param navStep pending steps. Positive = steps forward, negative = steps backward - */ -data class TransientWebState( - val targetUrl: String? = null, - val navStep: Int = 0, -) diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebStore.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebStore.kt index 5b39280ef..f5b0f1703 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebStore.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/FrostWebStore.kt @@ -17,6 +17,7 @@ package com.pitchedapps.frost.web.state import com.pitchedapps.frost.ext.WebTargetId +import com.pitchedapps.frost.web.state.state.SessionState import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.Store @@ -28,11 +29,12 @@ import mozilla.components.lib.state.Store */ class FrostWebStore( initialState: FrostWebState = FrostWebState(), + frostWebReducer: FrostWebReducer, middleware: List> = emptyList(), ) : Store( initialState, - FrostWebReducer::reduce, + frostWebReducer::reduce, middleware, "FrostStore", ) { @@ -41,7 +43,7 @@ class FrostWebStore( } } -operator fun FrostWebState.get(tabId: WebTargetId): TabWebState? { +operator fun FrostWebState.get(tabId: WebTargetId): SessionState? { if (floatingTab?.id == tabId) return floatingTab return homeTabs.find { it.id == tabId } } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/helper/Target.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/helper/Target.kt index 558e91add..9e92a9d1d 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/helper/Target.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/helper/Target.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.LifecycleOwner import com.pitchedapps.frost.ext.WebTargetId import com.pitchedapps.frost.web.state.FrostWebState import com.pitchedapps.frost.web.state.FrostWebStore -import com.pitchedapps.frost.web.state.TabWebState +import com.pitchedapps.frost.web.state.state.SessionState import mozilla.components.lib.state.Store import mozilla.components.lib.state.ext.observeAsComposableState @@ -35,20 +35,20 @@ import mozilla.components.lib.state.ext.observeAsComposableState */ sealed class Target { /** - * Looks up this target in the given [FrostWebStore] and returns the matching [TabWebState] if + * Looks up this target in the given [FrostWebStore] and returns the matching [SessionState] if * available. Otherwise returns `null`. * * @param store to lookup this target in. */ - fun lookupIn(store: FrostWebStore): TabWebState? = lookupIn(store.state) + fun lookupIn(store: FrostWebStore): SessionState? = lookupIn(store.state) /** - * Looks up this target in the given [FrostWebState] and returns the matching [TabWebState] if + * Looks up this target in the given [FrostWebState] and returns the matching [SessionState] if * available. Otherwise returns `null`. * * @param state to lookup this target in. */ - abstract fun lookupIn(state: FrostWebState): TabWebState? + abstract fun lookupIn(state: FrostWebState): SessionState? /** * Observes this target and represents the mapped state (using [map]) via [State]. @@ -60,14 +60,14 @@ sealed class Target { * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state. * * @param store that should get observed - * @param observe function that maps a [TabWebState] to the (sub) state that should get observed + * @param observe function that maps a [SessionState] to the (sub) state that should get observed * for changes. */ @Composable fun observeAsComposableStateFrom( store: FrostWebStore, - observe: (TabWebState?) -> R, - ): State { + observe: (SessionState?) -> R, + ): State { return store.observeAsComposableState( map = { state -> lookupIn(state) }, observe = { state -> observe(lookupIn(state)) }, @@ -75,13 +75,13 @@ sealed class Target { } data class HomeTab(val id: WebTargetId) : Target() { - override fun lookupIn(state: FrostWebState): TabWebState? { + override fun lookupIn(state: FrostWebState): SessionState? { return state.homeTabs.find { it.id == id } } } object FloatingTab : Target() { - override fun lookupIn(state: FrostWebState): TabWebState? { + override fun lookupIn(state: FrostWebState): SessionState? { return state.floatingTab } } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/ContentStateReducer.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/ContentStateReducer.kt index 365f94f08..14d4602e4 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/ContentStateReducer.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/ContentStateReducer.kt @@ -28,11 +28,13 @@ import com.pitchedapps.frost.web.state.TabAction.UserAction import com.pitchedapps.frost.web.state.TabAction.UserAction.GoBackAction import com.pitchedapps.frost.web.state.TabAction.UserAction.GoForwardAction import com.pitchedapps.frost.web.state.TabAction.UserAction.LoadUrlAction -import com.pitchedapps.frost.web.state.TabWebState -import com.pitchedapps.frost.web.state.TransientWebState +import com.pitchedapps.frost.web.state.state.ContentState +import com.pitchedapps.frost.web.state.state.TransientWebState +import javax.inject.Inject -internal object ContentStateReducer { - fun reduce(state: TabWebState, action: Action): TabWebState { +internal class ContentStateReducer @Inject internal constructor() { + + fun reduce(state: ContentState, action: Action): ContentState { return when (action) { is UpdateUrlAction -> state.copy(url = action.url) is UpdateProgressAction -> state.copy(progress = action.progress) diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/TabListReducer.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/TabListReducer.kt index ae5cda100..6ee0d6332 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/TabListReducer.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/TabListReducer.kt @@ -16,29 +16,42 @@ */ package com.pitchedapps.frost.web.state.reducer +import android.content.Context import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.facebook.tab import com.pitchedapps.frost.web.state.AuthWebState import com.pitchedapps.frost.web.state.FrostWebState import com.pitchedapps.frost.web.state.TabListAction import com.pitchedapps.frost.web.state.TabListAction.SetHomeTabs -import com.pitchedapps.frost.web.state.TabWebState +import com.pitchedapps.frost.web.state.state.ContentState +import com.pitchedapps.frost.web.state.state.HomeTabSessionState +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject -internal object TabListReducer { +internal class TabListReducer +@Inject +internal constructor( + @ApplicationContext private val context: Context, +) { fun reduce(state: FrostWebState, action: TabListAction): FrostWebState { return when (action) { is SetHomeTabs -> { - val tabs = action.data.mapIndexed { i, fbItem -> fbItem.toTab(i, state.auth) } + val tabs = + action.data.mapIndexed { i, fbItem -> fbItem.toHomeTabSession(context, i, state.auth) } state.copy(homeTabs = tabs) } + is TabListAction.SelectHomeTab -> state.copy(selectedHomeTab = action.id) } } } -private fun FbItem.toTab(i: Int, auth: AuthWebState): TabWebState = - TabWebState( - id = TabWebState.homeTabId(i), +private fun FbItem.toHomeTabSession( + context: Context, + i: Int, + auth: AuthWebState +): HomeTabSessionState = + HomeTabSessionState( userId = auth.currentUser, - baseUrl = url, - url = url, - icon = icon, + content = ContentState(url = url), + tab = tab(context, id = HomeTabSessionState.homeTabId(i)), ) diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/state/SessionState.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/state/SessionState.kt new file mode 100644 index 000000000..bb5671b67 --- /dev/null +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/state/SessionState.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2023 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.web.state.state + +import com.pitchedapps.frost.ext.WebTargetId +import com.pitchedapps.frost.main.MainTabItem +import com.pitchedapps.frost.web.state.AuthWebState.AuthUser + +/** Data representation of single session. */ +interface SessionState { + val id: WebTargetId + val userId: AuthUser + val content: ContentState + + fun createCopy( + id: WebTargetId = this.id, + userId: AuthUser = this.userId, + content: ContentState = this.content + ): SessionState +} + +/** Session for home screen, which includes nav bar data */ +data class HomeTabSessionState( + override val userId: AuthUser, + override val content: ContentState, + val tab: MainTabItem, +) : SessionState { + + override val id: WebTargetId + get() = tab.id + + override fun createCopy(id: WebTargetId, userId: AuthUser, content: ContentState) = + copy(userId = userId, content = content, tab = tab.copy(id = id)) + + companion object { + fun homeTabId(index: Int): WebTargetId = WebTargetId("home-tab--$index") + } +} + +data class FloatingTabSessionState( + override val id: WebTargetId, + override val userId: AuthUser, + override val content: ContentState, +) : SessionState { + override fun createCopy(id: WebTargetId, userId: AuthUser, content: ContentState) = + copy(id = id, userId = userId, content = content) +} + +/** Data relating to webview content */ +data class ContentState( + val url: String, + val title: String? = null, + val progress: Int = 0, + val loading: Boolean = false, + val canGoBack: Boolean = false, + val canGoForward: Boolean = false, + val transientState: TransientWebState = TransientWebState(), +) + +/** + * Transient web state. + * + * While we typically don't want to store this, our webview is not a composable, and requires a + * bridge to handle events. + * + * This state is not a list of pending actions, but rather a snapshot of the expected changes so + * that conflicting events can be ignored. + * + * @param targetUrl url destination if nonnull + * @param navStep pending steps. Positive = steps forward, negative = steps backward + */ +data class TransientWebState( + val targetUrl: String? = null, + val navStep: Int = 0, +) diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/webview/FrostChromeClients.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/webview/FrostChromeClients.kt index 2739664cb..bc819f3ff 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/webview/FrostChromeClients.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/webview/FrostChromeClients.kt @@ -55,7 +55,7 @@ class FrostChromeClient(private val tabId: WebTargetId, private val store: Frost override fun onProgressChanged(view: WebView, newProgress: Int) { super.onProgressChanged(view, newProgress) // TODO remove? - if (store.state[tabId]?.progress == 100) return + if (store.state[tabId]?.content?.progress == 100) return store.dispatch(UpdateProgressAction(newProgress)) }