1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-09-18 21:12:24 +02: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.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"),
),
)

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.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<TabWebState>.() -> Unit) = launch {
store.flow(lifecycleOwner).mapNotNull { it[tabId] }.action()
fun storeFlow(action: suspend Flow<ContentState>.() -> 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)
},
)
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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
)

View File

@ -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<MainTabItem>) {
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<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
private fun MainScreenWebContainer(
modifier: Modifier,
selectedTab: WebTargetId?,
items: List<MainTabItem>,
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") })
}

View File

@ -39,6 +39,8 @@ object InitAction : FrostWebAction
/** Actions affecting multiple tabs */
sealed interface TabListAction : FrostWebAction {
data class SetHomeTabs(val data: List<FbItem>) : TabListAction
data class SelectHomeTab(val id: WebTargetId) : TabListAction
}
/** 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.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<HomeTabSessionState>?
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<TabWebState>.updateTabs(
internal fun <T : SessionState> List<T>.updateTabs(
tabId: WebTargetId,
update: (TabWebState) -> TabWebState,
): List<TabWebState>? {
update: (T) -> T,
): List<SessionState>? {
val tabIndex = indexOfFirst { it.id == tabId }
if (tabIndex == -1) return null
return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size)
}

View File

@ -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<TabWebState> = emptyList(),
var floatingTab: TabWebState? = null,
val selectedHomeTab: WebTargetId? = null,
val homeTabs: List<HomeTabSessionState> = 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,
)

View File

@ -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<Middleware<FrostWebState, FrostWebAction>> = emptyList(),
) :
Store<FrostWebState, FrostWebAction>(
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 }
}

View File

@ -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 <R> observeAsComposableStateFrom(
store: FrostWebStore,
observe: (TabWebState?) -> R,
): State<TabWebState?> {
observe: (SessionState?) -> R,
): State<SessionState?> {
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
}
}

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.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)

View File

@ -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)),
)

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) {
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))
}