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:
parent
c010c67d33
commit
4a4fe97d08
@ -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"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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") })
|
|
||||||
}
|
|
||||||
|
@ -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 */
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
)
|
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
)
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user