From d88bde40675341bc1cd7a01b4a3a1762cd44f1f0 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 20 Jun 2023 02:45:53 -0700 Subject: [PATCH] Add home tab sample --- .../com/pitchedapps/frost/StartActivity.kt | 36 ++++++++-- .../pitchedapps/frost/main/MainActivity.kt | 6 +- .../com/pitchedapps/frost/main/MainScreen.kt | 18 ----- .../frost/main/MainScreenViewModel.kt | 10 ++- .../frost/main/MainScreenWebView.kt | 65 +++++++++++++++++++ .../frost/web/state/FrostWebAction.kt | 6 ++ .../frost/web/state/FrostWebReducer.kt | 2 + .../frost/web/state/FrostWebState.kt | 15 ++++- .../frost/web/state/reducer/TabListReducer.kt | 44 +++++++++++++ 9 files changed, 165 insertions(+), 37 deletions(-) create mode 100644 app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt create mode 100644 app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/TabListReducer.kt 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 6d62097b7..9facf7371 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -19,6 +19,7 @@ package com.pitchedapps.frost import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.google.common.flogger.FluentLogger import com.pitchedapps.frost.components.FrostDataStore import com.pitchedapps.frost.db.FrostDb @@ -26,12 +27,15 @@ import com.pitchedapps.frost.ext.FrostAccountId import com.pitchedapps.frost.ext.idData import com.pitchedapps.frost.ext.launchActivity import com.pitchedapps.frost.extension.FrostCoreExtension +import com.pitchedapps.frost.facebook.FbItem 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 dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -43,22 +47,44 @@ import kotlinx.coroutines.withContext * and will launch another activity without history after doing initialization work. */ @AndroidEntryPoint -class StartActivity : AppCompatActivity(), CoroutineScope by MainScope() { +class StartActivity : AppCompatActivity() { @Inject lateinit var frostDb: FrostDb + @Inject lateinit var dataStore: FrostDataStore + @Inject lateinit var frostCoreExtension: FrostCoreExtension + @Inject lateinit var store: FrostWebStore + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - launch { + lifecycleScope.launch { val id = withContext(Dispatchers.IO) { getCurrentAccountId() } - frostCoreExtension.install() + // frostCoreExtension.install() logger.atInfo().log("Starting Frost with id %s", id) + // TODO load real tabs + store.dispatch(TabListAction.SetHomeTabs(data = listOf(FbItem.Feed, FbItem.Menu))) + // Test something scrollable + store.dispatch( + TabAction( + tabId = TabWebState.homeTabId(0), + TabAction.ContentAction.UpdateUrlAction( + "https://github.com/AllanWang/Frost-for-Facebook" + ), + ), + ) + store.dispatch( + TabAction( + tabId = TabWebState.homeTabId(1), + TabAction.ContentAction.UpdateUrlAction("https://github.com/AllanWang/KAU"), + ), + ) + launchActivity( intentBuilder = { flags = 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 fedc18561..73fc7f523 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,8 +22,6 @@ 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.facebook.FbItem -import com.pitchedapps.frost.facebook.tab import dagger.hilt.android.AndroidEntryPoint /** @@ -40,14 +38,12 @@ class MainActivity : ComponentActivity() { logger.atInfo().log("onCreate main activity") WindowCompat.setDecorFitsSystemWindows(window, false) - val tabs = FbItem.defaults().map { it.tab(this) } // TODO allow custom tabs - setContent { FrostTheme { // MainScreen( // tabs = tabs, // ) - MainScreen2() + MainScreenWebView() } } } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreen.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreen.kt index 1623bd018..b6f4b0cc8 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreen.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.ContentAlpha import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text @@ -44,23 +43,6 @@ import com.pitchedapps.frost.ext.GeckoContextId import com.pitchedapps.frost.ext.components import mozilla.components.browser.state.helper.Target -@Composable -fun MainScreen2(modifier: Modifier = Modifier) { - val vm: MainScreenViewModel = viewModel() - Scaffold( - modifier = modifier, - topBar = { MainTopBar(modifier = modifier) }, - ) { paddingValues -> - vm.frostWebCompose.WebView(modifier = Modifier.padding(paddingValues)) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MainTopBar(modifier: Modifier = Modifier) { - TopAppBar(title = { Text(text = "Title") }) -} - /** * Screen for MainActivity. * diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenViewModel.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenViewModel.kt index 152e5a3a4..88aea4845 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenViewModel.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenViewModel.kt @@ -21,13 +21,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel -import com.pitchedapps.frost.compose.webview.FrostWebCompose import com.pitchedapps.frost.ext.GeckoContextId -import com.pitchedapps.frost.ext.WebTargetId import com.pitchedapps.frost.ext.idData import com.pitchedapps.frost.ext.toContextId import com.pitchedapps.frost.extension.FrostCoreExtension import com.pitchedapps.frost.hilt.FrostComponents +import com.pitchedapps.frost.web.state.FrostWebStore import com.pitchedapps.frost.webview.FrostWebComposer import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -42,14 +41,13 @@ internal constructor( @ApplicationContext context: Context, val components: FrostComponents, val frostCoreExtension: FrostCoreExtension, - // sample: FrostWebEntrySample, - frostWebComposer: FrostWebComposer, + val store: FrostWebStore, + val frostWebComposer: FrostWebComposer, +// sample: FrostWebEntrySample, ) : ViewModel() { val contextIdFlow: Flow = components.dataStore.account.idData.map { it?.toContextId() } var tabIndex: Int by mutableStateOf(0) - - val frostWebCompose: FrostWebCompose = frostWebComposer.create(WebTargetId("test")) } 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 new file mode 100644 index 000000000..605495f6e --- /dev/null +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt @@ -0,0 +1,65 @@ +/* + * 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.main + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import com.pitchedapps.frost.web.state.FrostWebStore +import com.pitchedapps.frost.webview.FrostWebComposer +import mozilla.components.lib.state.ext.observeAsState + +@Composable +fun MainScreenWebView(modifier: Modifier = Modifier) { + val vm: MainScreenViewModel = viewModel() + Scaffold( + modifier = modifier, + topBar = { MainTopBar(modifier = modifier) }, + ) { paddingValues -> + MainScreenWebContainer( + modifier = Modifier.padding(paddingValues), + store = vm.store, + frostWebComposer = vm.frostWebComposer, + ) + } +} + +@Composable +private fun MainScreenWebContainer( + modifier: Modifier, + store: FrostWebStore, + frostWebComposer: FrostWebComposer +) { + val homeTabs by store.observeAsState(initialValue = emptyList()) { it.homeTabs } + val homeTabComposables = remember(homeTabs) { homeTabs.map { frostWebComposer.create(it.id) } } + + Box(modifier = modifier) { homeTabComposables.firstOrNull()?.WebView() } +} + +@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 e0f8d696f..f4d11bbd0 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 @@ -17,6 +17,7 @@ package com.pitchedapps.frost.web.state import com.pitchedapps.frost.ext.WebTargetId +import com.pitchedapps.frost.facebook.FbItem import mozilla.components.lib.state.Action /** @@ -35,6 +36,11 @@ sealed interface FrostWebAction : Action */ object InitAction : FrostWebAction +/** Actions affecting multiple tabs */ +sealed interface TabListAction : FrostWebAction { + data class SetHomeTabs(val data: List) : TabListAction +} + /** Action affecting a single tab */ data class TabAction(val tabId: WebTargetId, val action: Action) : FrostWebAction { sealed interface Action 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 db3f68811..27f787831 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 @@ -18,6 +18,7 @@ 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 /** * See @@ -29,6 +30,7 @@ internal object FrostWebReducer { fun reduce(state: FrostWebState, action: FrostWebAction): FrostWebState { return when (action) { is InitAction -> state + is TabListAction -> TabListReducer.reduce(state, action) is TabAction -> state.updateTabState(action.tabId) { ContentStateReducer.reduce(it, action.action) } } 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 94c0cb2ed..b922a8241 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,6 +16,7 @@ */ 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 mozilla.components.lib.state.State @@ -60,14 +61,22 @@ data class AuthWebState( data class TabWebState( val id: WebTargetId, val userId: AuthWebState.AuthUser, - val baseUrl: String? = null, - val url: String? = null, + 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. 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 new file mode 100644 index 000000000..ae5cda100 --- /dev/null +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/web/state/reducer/TabListReducer.kt @@ -0,0 +1,44 @@ +/* + * 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.reducer + +import com.pitchedapps.frost.facebook.FbItem +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 + +internal object TabListReducer { + fun reduce(state: FrostWebState, action: TabListAction): FrostWebState { + return when (action) { + is SetHomeTabs -> { + val tabs = action.data.mapIndexed { i, fbItem -> fbItem.toTab(i, state.auth) } + state.copy(homeTabs = tabs) + } + } + } +} + +private fun FbItem.toTab(i: Int, auth: AuthWebState): TabWebState = + TabWebState( + id = TabWebState.homeTabId(i), + userId = auth.currentUser, + baseUrl = url, + url = url, + icon = icon, + )