1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-09 12:32:30 +01:00

Create session state and selected home tab

This commit is contained in:
Allan Wang 2023-06-20 14:33:53 -07:00
parent c010c67d33
commit 4a4fe97d08
No known key found for this signature in database
GPG Key ID: C93E3F9C679D7A56
16 changed files with 249 additions and 101 deletions

View File

@ -32,7 +32,7 @@ import com.pitchedapps.frost.main.MainActivity
import com.pitchedapps.frost.web.state.FrostWebStore import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.web.state.TabAction import com.pitchedapps.frost.web.state.TabAction
import com.pitchedapps.frost.web.state.TabListAction 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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -72,7 +72,7 @@ class StartActivity : AppCompatActivity() {
// Test something scrollable // Test something scrollable
store.dispatch( store.dispatch(
TabAction( TabAction(
tabId = TabWebState.homeTabId(0), tabId = HomeTabSessionState.homeTabId(0),
TabAction.ContentAction.UpdateUrlAction( TabAction.ContentAction.UpdateUrlAction(
"https://github.com/AllanWang/Frost-for-Facebook" "https://github.com/AllanWang/Frost-for-Facebook"
), ),
@ -80,7 +80,7 @@ class StartActivity : AppCompatActivity() {
) )
store.dispatch( store.dispatch(
TabAction( TabAction(
tabId = TabWebState.homeTabId(1), tabId = HomeTabSessionState.homeTabId(1),
TabAction.ContentAction.UpdateUrlAction("https://github.com/AllanWang/KAU"), TabAction.ContentAction.UpdateUrlAction("https://github.com/AllanWang/KAU"),
), ),
) )

View File

@ -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
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.WebStepResponseAction 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.get
import com.pitchedapps.frost.web.state.state.ContentState
import com.pitchedapps.frost.webview.FrostChromeClient import com.pitchedapps.frost.webview.FrostChromeClient
import com.pitchedapps.frost.webview.FrostWebViewClient import com.pitchedapps.frost.webview.FrostWebViewClient
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -88,13 +88,14 @@ class FrostWebCompose(
webView?.let { wv -> webView?.let { wv ->
val lifecycleOwner = LocalLifecycleOwner.current 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() } BackHandler(captureBackPresses && canGoBack) { wv.goBack() }
LaunchedEffect(wv, store) { LaunchedEffect(wv, store) {
fun storeFlow(action: suspend Flow<TabWebState>.() -> Unit) = launch { fun storeFlow(action: suspend Flow<ContentState>.() -> Unit) = launch {
store.flow(lifecycleOwner).mapNotNull { it[tabId] }.action() store.flow(lifecycleOwner).mapNotNull { it[tabId]?.content }.action()
} }
storeFlow { storeFlow {
@ -128,6 +129,8 @@ class FrostWebCompose(
.apply { .apply {
onCreated(this) onCreated(this)
logger.atInfo().log("Created webview for %s", tabId)
this.layoutParams = this.layoutParams =
FrameLayout.LayoutParams( FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT,
@ -141,7 +144,7 @@ class FrostWebCompose(
webChromeClient = chromeClient webChromeClient = chromeClient
webViewClient = client webViewClient = client
val url = store.state[tabId]?.url val url = store.state[tabId]?.content?.url
if (url != null) loadUrl(url) if (url != null) loadUrl(url)
} }
.also { webView = it } .also { webView = it }
@ -163,6 +166,7 @@ class FrostWebCompose(
onRelease = { parentFrame -> onRelease = { parentFrame ->
val wv = parentFrame.children.first() as WebView val wv = parentFrame.children.first() as WebView
onDispose(wv) onDispose(wv)
logger.atInfo().log("Released webview for %s", tabId)
}, },
) )
} }

View File

@ -39,6 +39,7 @@ import androidx.compose.material.icons.filled.Store
import androidx.compose.material.icons.filled.Today import androidx.compose.material.icons.filled.Today
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import com.pitchedapps.frost.R import com.pitchedapps.frost.R
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.main.MainTabItem import com.pitchedapps.frost.main.MainTabItem
import compose.icons.FontAwesomeIcons import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Solid import compose.icons.fontawesomeicons.Solid
@ -118,8 +119,9 @@ enum class FbItem(
} }
/** Converts [FbItem] to [MainTabItem]. */ /** Converts [FbItem] to [MainTabItem]. */
fun FbItem.tab(context: Context): MainTabItem = fun FbItem.tab(context: Context, id: WebTargetId): MainTabItem =
MainTabItem( MainTabItem(
id = id,
title = context.getString(titleId), title = context.getString(titleId),
icon = icon, icon = icon,
url = url, url = url,

View File

@ -20,6 +20,7 @@ import android.webkit.CookieManager
import com.google.common.flogger.FluentLogger import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.web.state.FrostLoggerMiddleware import com.pitchedapps.frost.web.state.FrostLoggerMiddleware
import com.pitchedapps.frost.web.state.FrostWebReducer
import com.pitchedapps.frost.web.state.FrostWebStore import com.pitchedapps.frost.web.state.FrostWebStore
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -38,11 +39,9 @@ object FrostWebViewModule {
@Provides @Provides
@Singleton @Singleton
fun frostWebStore(): FrostWebStore { fun frostWebStore(frostWebReducer: FrostWebReducer): FrostWebStore {
val middleware = buildList { if (BuildConfig.DEBUG) add(FrostLoggerMiddleware()) } val middleware = buildList { if (BuildConfig.DEBUG) add(FrostLoggerMiddleware()) }
val store = FrostWebStore(middleware = middleware) return FrostWebStore(frostWebReducer = frostWebReducer, middleware = middleware)
return store
} }
} }

View File

@ -22,7 +22,10 @@ import androidx.activity.compose.setContent
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.google.common.flogger.FluentLogger import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.compose.FrostTheme import com.pitchedapps.frost.compose.FrostTheme
import com.pitchedapps.frost.web.state.FrostWebStore
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import mozilla.components.lib.state.ext.observeAsState
/** /**
* Main activity. * Main activity.
@ -32,6 +35,8 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@Inject lateinit var store: FrostWebStore
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -43,7 +48,12 @@ class MainActivity : ComponentActivity() {
// MainScreen( // MainScreen(
// tabs = tabs, // tabs = tabs,
// ) // )
MainScreenWebView()
val tabs =
store.observeAsState(initialValue = null) { it.homeTabs.map { it.tab } }.value
?: return@FrostTheme
MainScreenWebView(homeTabs = tabs)
} }
} }
} }

View File

@ -17,6 +17,12 @@
package com.pitchedapps.frost.main package com.pitchedapps.frost.main
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import com.pitchedapps.frost.ext.WebTargetId
/** Data representation of a single main tab entry. */ /** 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
)

View File

@ -23,6 +23,9 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api 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.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@ -35,37 +38,81 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel 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.FrostWebStore
import com.pitchedapps.frost.web.state.TabListAction.SelectHomeTab
import com.pitchedapps.frost.webview.FrostWebComposer import com.pitchedapps.frost.webview.FrostWebComposer
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.observeAsState import mozilla.components.lib.state.ext.observeAsState
@Composable @Composable
fun MainScreenWebView(modifier: Modifier = Modifier) { fun MainScreenWebView(modifier: Modifier = Modifier, homeTabs: List<MainTabItem>) {
val vm: MainScreenViewModel = viewModel() val vm: MainScreenViewModel = viewModel()
val selectedHomeTab by vm.store.observeAsState(initialValue = null) { it.selectedHomeTab }
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = { MainTopBar(modifier = modifier) }, topBar = { MainTopBar(modifier = modifier) },
bottomBar = {
MainBottomBar(
selectedTab = selectedHomeTab,
items = homeTabs,
onSelect = { vm.store.dispatch(SelectHomeTab(it)) },
)
},
) { paddingValues -> ) { paddingValues ->
MainScreenWebContainer( MainScreenWebContainer(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
selectedTab = selectedHomeTab,
items = homeTabs,
store = vm.store, store = vm.store,
frostWebComposer = vm.frostWebComposer, 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<MainTabItem>,
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 @Composable
private fun MainScreenWebContainer( private fun MainScreenWebContainer(
modifier: Modifier, modifier: Modifier,
selectedTab: WebTargetId?,
items: List<MainTabItem>,
store: FrostWebStore, store: FrostWebStore,
frostWebComposer: FrostWebComposer frostWebComposer: FrostWebComposer
) { ) {
val homeTabs by store.observeAsState(initialValue = emptyList()) { it.homeTabs } val homeTabComposables = remember(items) { items.map { frostWebComposer.create(it.id) } }
val homeTabComposables = remember(homeTabs) { homeTabs.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) @OptIn(ExperimentalMaterialApi::class)
@ -89,9 +136,3 @@ private fun PullRefresh(modifier: Modifier, store: FrostWebStore, content: @Comp
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainTopBar(modifier: Modifier = Modifier) {
TopAppBar(title = { Text(text = "Title") })
}

View File

@ -39,6 +39,8 @@ object InitAction : FrostWebAction
/** Actions affecting multiple tabs */ /** Actions affecting multiple tabs */
sealed interface TabListAction : FrostWebAction { sealed interface TabListAction : FrostWebAction {
data class SetHomeTabs(val data: List<FbItem>) : TabListAction data class SetHomeTabs(val data: List<FbItem>) : TabListAction
data class SelectHomeTab(val id: WebTargetId) : TabListAction
} }
/** Action affecting a single tab */ /** Action affecting a single tab */

View File

@ -19,6 +19,10 @@ package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.ext.WebTargetId import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.web.state.reducer.ContentStateReducer import com.pitchedapps.frost.web.state.reducer.ContentStateReducer
import com.pitchedapps.frost.web.state.reducer.TabListReducer 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 * See
@ -26,25 +30,35 @@ import com.pitchedapps.frost.web.state.reducer.TabListReducer
* *
* For firefox example * 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 { fun reduce(state: FrostWebState, action: FrostWebAction): FrostWebState {
return when (action) { return when (action) {
is InitAction -> state is InitAction -> state
is TabListAction -> TabListReducer.reduce(state, action) is TabListAction -> tabListReducer.reduce(state, action)
is TabAction -> 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( internal fun FrostWebState.updateTabState(
tabId: WebTargetId, tabId: WebTargetId,
update: (TabWebState) -> TabWebState, update: (SessionState) -> SessionState,
): FrostWebState { ): FrostWebState {
val floatingTabMatch = floatingTab?.takeIf { it.id == tabId } 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<HomeTabSessionState>?
if (newHomeTabs != null) return copy(homeTabs = newHomeTabs) if (newHomeTabs != null) return copy(homeTabs = newHomeTabs)
return this return this
} }
@ -55,12 +69,11 @@ internal fun FrostWebState.updateTabState(
* @param tabId ID of the tab to change. * @param tabId ID of the tab to change.
* @param update Returns a new version of the tab state. * @param update Returns a new version of the tab state.
*/ */
internal fun List<TabWebState>.updateTabs( internal fun <T : SessionState> List<T>.updateTabs(
tabId: WebTargetId, tabId: WebTargetId,
update: (TabWebState) -> TabWebState, update: (T) -> T,
): List<TabWebState>? { ): List<SessionState>? {
val tabIndex = indexOfFirst { it.id == tabId } val tabIndex = indexOfFirst { it.id == tabId }
if (tabIndex == -1) return null if (tabIndex == -1) return null
return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size) return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size)
} }

View File

@ -16,9 +16,10 @@
*/ */
package com.pitchedapps.frost.web.state package com.pitchedapps.frost.web.state
import androidx.compose.ui.graphics.vector.ImageVector
import com.pitchedapps.frost.ext.FrostAccountId import com.pitchedapps.frost.ext.FrostAccountId
import com.pitchedapps.frost.ext.WebTargetId 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 import mozilla.components.lib.state.State
/** /**
@ -29,8 +30,9 @@ import mozilla.components.lib.state.State
*/ */
data class FrostWebState( data class FrostWebState(
val auth: AuthWebState = AuthWebState(), val auth: AuthWebState = AuthWebState(),
val homeTabs: List<TabWebState> = emptyList(), val selectedHomeTab: WebTargetId? = null,
var floatingTab: TabWebState? = null, val homeTabs: List<HomeTabSessionState> = emptyList(),
var floatingTab: FloatingTabSessionState? = null,
) : State ) : State
/** /**
@ -57,40 +59,3 @@ data class AuthWebState(
object Unknown : AuthUser 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,
)

View File

@ -17,6 +17,7 @@
package com.pitchedapps.frost.web.state package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.ext.WebTargetId 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.Middleware
import mozilla.components.lib.state.Store import mozilla.components.lib.state.Store
@ -28,11 +29,12 @@ import mozilla.components.lib.state.Store
*/ */
class FrostWebStore( class FrostWebStore(
initialState: FrostWebState = FrostWebState(), initialState: FrostWebState = FrostWebState(),
frostWebReducer: FrostWebReducer,
middleware: List<Middleware<FrostWebState, FrostWebAction>> = emptyList(), middleware: List<Middleware<FrostWebState, FrostWebAction>> = emptyList(),
) : ) :
Store<FrostWebState, FrostWebAction>( Store<FrostWebState, FrostWebAction>(
initialState, initialState,
FrostWebReducer::reduce, frostWebReducer::reduce,
middleware, middleware,
"FrostStore", "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 if (floatingTab?.id == tabId) return floatingTab
return homeTabs.find { it.id == tabId } return homeTabs.find { it.id == tabId }
} }

View File

@ -23,7 +23,7 @@ import androidx.lifecycle.LifecycleOwner
import com.pitchedapps.frost.ext.WebTargetId import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.web.state.FrostWebState import com.pitchedapps.frost.web.state.FrostWebState
import com.pitchedapps.frost.web.state.FrostWebStore 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.Store
import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.lib.state.ext.observeAsComposableState
@ -35,20 +35,20 @@ import mozilla.components.lib.state.ext.observeAsComposableState
*/ */
sealed class Target { 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`. * available. Otherwise returns `null`.
* *
* @param store to lookup this target in. * @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`. * available. Otherwise returns `null`.
* *
* @param state to lookup this target in. * @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]. * 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. * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
* *
* @param store that should get observed * @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. * for changes.
*/ */
@Composable @Composable
fun <R> observeAsComposableStateFrom( fun <R> observeAsComposableStateFrom(
store: FrostWebStore, store: FrostWebStore,
observe: (TabWebState?) -> R, observe: (SessionState?) -> R,
): State<TabWebState?> { ): State<SessionState?> {
return store.observeAsComposableState( return store.observeAsComposableState(
map = { state -> lookupIn(state) }, map = { state -> lookupIn(state) },
observe = { state -> observe(lookupIn(state)) }, observe = { state -> observe(lookupIn(state)) },
@ -75,13 +75,13 @@ sealed class Target {
} }
data class HomeTab(val id: WebTargetId) : 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 } return state.homeTabs.find { it.id == id }
} }
} }
object FloatingTab : Target() { object FloatingTab : Target() {
override fun lookupIn(state: FrostWebState): TabWebState? { override fun lookupIn(state: FrostWebState): SessionState? {
return state.floatingTab return state.floatingTab
} }
} }

View File

@ -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.GoBackAction
import com.pitchedapps.frost.web.state.TabAction.UserAction.GoForwardAction import com.pitchedapps.frost.web.state.TabAction.UserAction.GoForwardAction
import com.pitchedapps.frost.web.state.TabAction.UserAction.LoadUrlAction import com.pitchedapps.frost.web.state.TabAction.UserAction.LoadUrlAction
import com.pitchedapps.frost.web.state.TabWebState import com.pitchedapps.frost.web.state.state.ContentState
import com.pitchedapps.frost.web.state.TransientWebState import com.pitchedapps.frost.web.state.state.TransientWebState
import javax.inject.Inject
internal object ContentStateReducer { internal class ContentStateReducer @Inject internal constructor() {
fun reduce(state: TabWebState, action: Action): TabWebState {
fun reduce(state: ContentState, action: Action): ContentState {
return when (action) { return when (action) {
is UpdateUrlAction -> state.copy(url = action.url) is UpdateUrlAction -> state.copy(url = action.url)
is UpdateProgressAction -> state.copy(progress = action.progress) is UpdateProgressAction -> state.copy(progress = action.progress)

View File

@ -16,29 +16,42 @@
*/ */
package com.pitchedapps.frost.web.state.reducer package com.pitchedapps.frost.web.state.reducer
import android.content.Context
import com.pitchedapps.frost.facebook.FbItem 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.AuthWebState
import com.pitchedapps.frost.web.state.FrostWebState import com.pitchedapps.frost.web.state.FrostWebState
import com.pitchedapps.frost.web.state.TabListAction import com.pitchedapps.frost.web.state.TabListAction
import com.pitchedapps.frost.web.state.TabListAction.SetHomeTabs 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 { fun reduce(state: FrostWebState, action: TabListAction): FrostWebState {
return when (action) { return when (action) {
is SetHomeTabs -> { 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) state.copy(homeTabs = tabs)
} }
is TabListAction.SelectHomeTab -> state.copy(selectedHomeTab = action.id)
} }
} }
} }
private fun FbItem.toTab(i: Int, auth: AuthWebState): TabWebState = private fun FbItem.toHomeTabSession(
TabWebState( context: Context,
id = TabWebState.homeTabId(i), i: Int,
auth: AuthWebState
): HomeTabSessionState =
HomeTabSessionState(
userId = auth.currentUser, userId = auth.currentUser,
baseUrl = url, content = ContentState(url = url),
url = url, tab = tab(context, id = HomeTabSessionState.homeTabId(i)),
icon = icon,
) )

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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,
)

View File

@ -55,7 +55,7 @@ class FrostChromeClient(private val tabId: WebTargetId, private val store: Frost
override fun onProgressChanged(view: WebView, newProgress: Int) { override fun onProgressChanged(view: WebView, newProgress: Int) {
super.onProgressChanged(view, newProgress) super.onProgressChanged(view, newProgress)
// TODO remove? // TODO remove?
if (store.state[tabId]?.progress == 100) return if (store.state[tabId]?.content?.progress == 100) return
store.dispatch(UpdateProgressAction(newProgress)) store.dispatch(UpdateProgressAction(newProgress))
} }