mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-11-08 12:02:33 +01:00
Merge pull request #1946 from AllanWang/gecko-removal
This commit is contained in:
commit
6beb8e05b6
@ -175,53 +175,14 @@ dependencies {
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
||||
|
||||
// https://maven.mozilla.org/?prefix=maven2/org/mozilla/geckoview/geckoview-beta/
|
||||
def geckoviewChannel = "beta"
|
||||
def geckoviewVersion = "105.0.20220908185813"
|
||||
// implementation "org.mozilla.geckoview:geckoview-${geckoviewChannel}:${geckoviewVersion}"
|
||||
|
||||
// https://maven.mozilla.org/maven2/org/mozilla/components/browser-engine-gecko/maven-metadata.xml
|
||||
// https://github.com/mozilla-mobile/reference-browser/blob/master/buildSrc/src/main/java/AndroidComponents.kt
|
||||
// https://nightly.maven.mozilla.org/maven2/org/mozilla/components/browser-engine-gecko/maven-metadata.xml
|
||||
def mozillaAndroidComponents = "116.0.20230617040331"
|
||||
|
||||
implementation "org.mozilla.components:browser-engine-gecko:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:browser-domains:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:browser-icons:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:browser-menu:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:browser-session-storage:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:browser-state:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:browser-toolbar:${mozillaAndroidComponents}"
|
||||
|
||||
implementation "org.mozilla.components:concept-engine:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:concept-menu:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:concept-storage:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:concept-toolbar:${mozillaAndroidComponents}"
|
||||
|
||||
implementation "org.mozilla.components:feature-app-links:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-addons:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-autofill:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-awesomebar:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-contextmenu:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-downloads:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-findinpage:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-logins:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-media:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-prompts:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-push:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-session:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-sitepermissions:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-tabs:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-toolbar:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:feature-webnotifications:${mozillaAndroidComponents}"
|
||||
|
||||
implementation "org.mozilla.components:support-ktx:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:support-webextensions:${mozillaAndroidComponents}"
|
||||
|
||||
implementation "org.mozilla.components:ui-autocomplete:${mozillaAndroidComponents}"
|
||||
|
||||
implementation "org.mozilla.components:compose-engine:${mozillaAndroidComponents}"
|
||||
implementation "org.mozilla.components:lib-state:${mozillaAndroidComponents}"
|
||||
// Kept for reference; not needed
|
||||
implementation "org.mozilla.components:browser-state:${mozillaAndroidComponents}"
|
||||
|
||||
// Compose
|
||||
def composeVersion = "1.4.3"
|
||||
|
@ -26,7 +26,6 @@ import com.pitchedapps.frost.db.FrostDb
|
||||
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
|
||||
@ -53,8 +52,6 @@ class StartActivity : AppCompatActivity() {
|
||||
|
||||
@Inject lateinit var dataStore: FrostDataStore
|
||||
|
||||
@Inject lateinit var frostCoreExtension: FrostCoreExtension
|
||||
|
||||
@Inject lateinit var store: FrostWebStore
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -63,8 +60,6 @@ class StartActivity : AppCompatActivity() {
|
||||
lifecycleScope.launch {
|
||||
val id = withContext(Dispatchers.IO) { getCurrentAccountId() }
|
||||
|
||||
// frostCoreExtension.install()
|
||||
|
||||
logger.atInfo().log("Starting Frost with id %s", id)
|
||||
|
||||
// TODO load real tabs
|
||||
|
@ -16,14 +16,10 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.components
|
||||
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
import mozilla.components.browser.session.storage.SessionStorage
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.permission.SitePermissionsStorage
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
|
||||
/**
|
||||
* Core injections.
|
||||
@ -34,24 +30,8 @@ import org.mozilla.geckoview.GeckoRuntime
|
||||
class Core
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val runtimeProvider: Provider<GeckoRuntime>,
|
||||
private val engineProvider: Provider<Engine>,
|
||||
private val storeProvider: Provider<BrowserStore>,
|
||||
private val sessionStorageProvider: Provider<SessionStorage>,
|
||||
private val sitePermissionsStorageProvider: Provider<SitePermissionsStorage>,
|
||||
private val storeProvider: Provider<FrostWebStore>,
|
||||
) {
|
||||
val runtime: GeckoRuntime
|
||||
get() = runtimeProvider.get()
|
||||
|
||||
val engine: Engine
|
||||
get() = engineProvider.get()
|
||||
|
||||
val store: BrowserStore
|
||||
val store: FrostWebStore
|
||||
get() = storeProvider.get()
|
||||
|
||||
val sessionStorage: SessionStorage
|
||||
get() = sessionStorageProvider.get()
|
||||
|
||||
val sitePermissionsStorage: SitePermissionsStorage
|
||||
get() = sitePermissionsStorageProvider.get()
|
||||
}
|
||||
|
@ -16,12 +16,9 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.components
|
||||
|
||||
import com.pitchedapps.frost.components.usecases.FloatingTabsUseCases
|
||||
import com.pitchedapps.frost.components.usecases.HomeTabsUseCases
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
|
||||
/**
|
||||
* Collection of frost use cases.
|
||||
@ -32,8 +29,5 @@ import mozilla.components.feature.tabs.TabsUseCases
|
||||
class UseCases
|
||||
@Inject
|
||||
internal constructor(
|
||||
val session: SessionUseCases,
|
||||
val tabs: TabsUseCases,
|
||||
val homeTabs: HomeTabsUseCases,
|
||||
val floatingTabs: FloatingTabsUseCases,
|
||||
)
|
||||
|
@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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.components.usecases
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import mozilla.components.browser.state.action.EngineAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
|
||||
/** Use cases for the floating tabs, located in an overlay activity. */
|
||||
@Singleton
|
||||
class FloatingTabsUseCases @Inject internal constructor(private val store: BrowserStore) {
|
||||
|
||||
/**
|
||||
* Create or update the floating tab url.
|
||||
*
|
||||
* There is at most one floating tab at all times.
|
||||
*
|
||||
* TODO: Add context id.
|
||||
*/
|
||||
fun createFloatingTab(url: String) {
|
||||
if (store.state.findTab(TAB_ID) == null) {
|
||||
val tab = createTab(url = url, id = TAB_ID)
|
||||
store.dispatch(TabListAction.AddTabAction(tab = tab, select = false))
|
||||
}
|
||||
store.dispatch(EngineAction.LoadUrlAction(tabId = TAB_ID, url = url))
|
||||
}
|
||||
|
||||
/** Remove floating tab screen. */
|
||||
fun removeFloatingTab() {
|
||||
store.dispatch(TabListAction.RemoveTabAction(tabId = TAB_ID))
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Unique tab id for floating screen. */
|
||||
const val TAB_ID = "floating_tab_id"
|
||||
}
|
||||
}
|
@ -16,48 +16,23 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.components.usecases
|
||||
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.ext.GeckoContextId
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabListAction.SelectHomeTab
|
||||
import com.pitchedapps.frost.web.state.TabListAction.SetHomeTabs
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.EngineAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
|
||||
/** Use cases for the home screen. */
|
||||
@Singleton
|
||||
class HomeTabsUseCases @Inject internal constructor(private val store: BrowserStore) {
|
||||
class HomeTabsUseCases @Inject internal constructor(private val store: FrostWebStore) {
|
||||
|
||||
/**
|
||||
* Create the provided tabs.
|
||||
*
|
||||
* If there are existing tabs, they will be replaced.
|
||||
*/
|
||||
fun createHomeTabs(
|
||||
contextId: GeckoContextId,
|
||||
selectedIndex: Int,
|
||||
urls: List<String>
|
||||
): List<TabSessionState> {
|
||||
store.dispatch(TabListAction.RemoveAllTabsAction())
|
||||
if (urls.isEmpty()) return emptyList()
|
||||
val tabs =
|
||||
urls.mapIndexed { i, url -> createTab(id = tabId(i), url = url, contextId = contextId.id) }
|
||||
store.dispatch(TabListAction.AddMultipleTabsAction(tabs))
|
||||
// Preload all tabs
|
||||
for (tab in tabs) {
|
||||
store.dispatch(EngineAction.LoadUrlAction(tab.id, tab.content.url))
|
||||
// if (tab.content.url == MESSENGER_URL) {
|
||||
// store.dispatch(EngineAction.ToggleDesktopModeAction(tab.id, enable = true))
|
||||
// }
|
||||
}
|
||||
selectHomeTab(selectedIndex)
|
||||
return tabs
|
||||
fun createHomeTabs(items: List<FbItem>) {
|
||||
store.dispatch(SetHomeTabs(items))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,51 +40,7 @@ class HomeTabsUseCases @Inject internal constructor(private val store: BrowserSt
|
||||
*
|
||||
* If the index is OOB, the selected tab will be null.
|
||||
*/
|
||||
fun selectHomeTab(index: Int) {
|
||||
store.dispatch(TabListAction.SelectTabAction(tabId(index)))
|
||||
}
|
||||
|
||||
/** Reload tab contents based on index. */
|
||||
fun reloadTab(index: Int) {
|
||||
store.dispatch(EngineAction.ReloadAction(tabId(index)))
|
||||
}
|
||||
|
||||
// Cannot use injection
|
||||
class HomeMiddleware : Middleware<BrowserState, BrowserAction> {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<BrowserState, BrowserAction>,
|
||||
next: (BrowserAction) -> Unit,
|
||||
action: BrowserAction
|
||||
) {
|
||||
// if (action is ContentAction.UpdateUrlAction) {
|
||||
// logger.atInfo().log("url update %s", action)
|
||||
// if (action.sessionId == context.state.tabs[0].id) {
|
||||
// val customTab = context.state.tabs[3]
|
||||
// context.dispatch(EngineAction.LoadUrlAction(tabId = customTab.id, url =
|
||||
// action.url))
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
next(action)
|
||||
// when (action) {
|
||||
// is ContentAction.UpdateUrlAction -> {
|
||||
// logger.atInfo().log("url update %s", action)
|
||||
// action.sessionId
|
||||
// }
|
||||
// else -> next(action)
|
||||
// }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFIX = "frost-home"
|
||||
|
||||
private fun isHomeTab(id: String): Boolean = id.startsWith(PREFIX)
|
||||
|
||||
private fun tabId(index: Int) = "$PREFIX--$index"
|
||||
fun selectHomeTab(tabId: WebTargetId) {
|
||||
store.dispatch(SelectHomeTab(tabId))
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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.components.usecases
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.feature.app.links.AppLinksUseCases
|
||||
import mozilla.components.feature.contextmenu.ContextMenuUseCases
|
||||
import mozilla.components.feature.downloads.DownloadsUseCases
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
|
||||
/** Module for Mozilla use cases. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class UseCasesModule {
|
||||
|
||||
@Provides @Singleton fun sessionUseCases(store: BrowserStore) = SessionUseCases(store)
|
||||
|
||||
@Provides @Singleton fun tabsUseCases(store: BrowserStore) = TabsUseCases(store)
|
||||
|
||||
@Provides @Singleton fun contextMenuUseCases(store: BrowserStore) = ContextMenuUseCases(store)
|
||||
|
||||
@Provides @Singleton fun downloadsUseCases(store: BrowserStore) = DownloadsUseCases(store)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun appLinksUseCases(@ApplicationContext context: Context) = AppLinksUseCases(context)
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
/*
|
||||
* 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.compose
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.zIndex
|
||||
import mozilla.components.browser.state.action.EngineAction
|
||||
import mozilla.components.browser.state.helper.Target
|
||||
import mozilla.components.browser.state.state.SessionState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
|
||||
/**
|
||||
* Simple Frost web view, with progress bar.
|
||||
*
|
||||
* TODO add swipe?
|
||||
*/
|
||||
@Composable
|
||||
fun FrostWeb(engine: Engine, store: BrowserStore, target: Target, modifier: Modifier = Modifier) {
|
||||
val selectedTab by
|
||||
target.observeAsComposableStateFrom(
|
||||
store = store,
|
||||
observe = { tab ->
|
||||
// Render if the tab itself changed or when the state of the linked engine session changes
|
||||
arrayOf(
|
||||
tab?.id,
|
||||
tab?.engineState?.engineSession,
|
||||
tab?.engineState?.crashed,
|
||||
tab?.content?.firstContentfulPaint,
|
||||
// Added on top of Mozilla's WebContent observe values
|
||||
tab?.content?.progress,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
ProgressBar(progress = selectedTab?.content?.progress, modifier = Modifier.zIndex(1f))
|
||||
WebContent(engine = engine, store = store, state = WebContentState(selectedTab))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressBar(progress: Int?, modifier: Modifier = Modifier) {
|
||||
val shouldDisplay = progress != null && progress in 0 until 100
|
||||
LinearProgressIndicator(
|
||||
modifier = modifier.alpha(if (shouldDisplay) 1f else 0f).height(2.dp),
|
||||
progress = if (progress == null) 0f else progress.toFloat() * 0.01f,
|
||||
)
|
||||
}
|
||||
|
||||
/** Minimum amount of [SessionState] needed to update [WebContent] */
|
||||
private data class WebContentState(
|
||||
val id: String,
|
||||
val engineSession: EngineSession?,
|
||||
val canGoBack: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
operator fun invoke(tab: SessionState?): WebContentState? {
|
||||
tab ?: return null
|
||||
return WebContentState(
|
||||
id = tab.id,
|
||||
engineSession = tab.engineState.engineSession,
|
||||
canGoBack = tab.content.canGoBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on Mozilla WebContent:
|
||||
* https://github.com/mozilla-mobile/android-components/blob/main/components/compose/engine/src/main/java/mozilla/components/compose/engine/WebContent.kt
|
||||
*
|
||||
* WebView from Accompanist:
|
||||
* https://github.com/google/accompanist/blob/main/web/src/main/java/com/google/accompanist/web/WebView.kt
|
||||
*
|
||||
* Blinking Bug: Switching tabs causes an empty state where nothing is rendered. Not as noticeable
|
||||
* on light themes, but it's a full black screen on dark mode. May be related to
|
||||
* https://github.com/mozilla-mobile/fenix/issues/1901
|
||||
*/
|
||||
@Composable
|
||||
private fun WebContent(engine: Engine, store: BrowserStore, state: WebContentState?) {
|
||||
|
||||
BackHandler(state?.canGoBack == true) {
|
||||
val id = state?.id ?: return@BackHandler
|
||||
store.dispatch(EngineAction.GoBackAction(id, userInteraction = true))
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context -> engine.createView(context).asView() },
|
||||
update = { view ->
|
||||
val engineView = view as EngineView
|
||||
if (state == null) {
|
||||
engineView.release()
|
||||
} else {
|
||||
val session = state.engineSession
|
||||
if (session == null) {
|
||||
// This tab does not have an EngineSession that we can render yet. Let's dispatch an
|
||||
// action to request creating one. Once one was created and linked to this session, this
|
||||
// method will get invoked again.
|
||||
store.dispatch(EngineAction.CreateEngineSessionAction(state.id))
|
||||
} else {
|
||||
engineView.render(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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.extension
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.feature.addons.AddonManager
|
||||
import mozilla.components.feature.addons.AddonsProvider
|
||||
import mozilla.components.feature.addons.amo.AddonCollectionProvider
|
||||
import mozilla.components.feature.addons.update.AddonUpdater
|
||||
import mozilla.components.feature.addons.update.DefaultAddonUpdater
|
||||
import mozilla.components.support.base.android.NotificationsDelegate
|
||||
|
||||
/** Injections related to addons. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FrostAddOnsModule {
|
||||
|
||||
// https://services.addons.mozilla.org/api/v4/accounts/account/17569911/collections/Frost-for-Facebook/addons/?page_size=10&lang=en-US
|
||||
@Provides
|
||||
@Singleton
|
||||
fun addonsProvider(@ApplicationContext context: Context, client: Client): AddonsProvider {
|
||||
return AddonCollectionProvider(
|
||||
context = context,
|
||||
client = client,
|
||||
collectionUser = "17569911",
|
||||
collectionName = "Frost-for-Facebook",
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun addonUpdater(
|
||||
@ApplicationContext context: Context,
|
||||
notificationsDelegate: NotificationsDelegate
|
||||
): AddonUpdater {
|
||||
return DefaultAddonUpdater(
|
||||
applicationContext = context,
|
||||
notificationsDelegate = notificationsDelegate
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun addonManager(
|
||||
store: BrowserStore,
|
||||
engine: Engine,
|
||||
addonsProvider: AddonsProvider,
|
||||
addonUpdater: AddonUpdater
|
||||
): AddonManager {
|
||||
return AddonManager(
|
||||
store = store,
|
||||
runtime = engine,
|
||||
addonsProvider = addonsProvider,
|
||||
addonUpdater = addonUpdater
|
||||
)
|
||||
}
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
/*
|
||||
* 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.extension
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.webextension.MessageHandler
|
||||
import mozilla.components.concept.engine.webextension.Port
|
||||
import mozilla.components.lib.state.ext.flow
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||
import mozilla.components.support.webextensions.WebExtensionController
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Frost's built in extension.
|
||||
*
|
||||
* Structure based off of
|
||||
*
|
||||
* https://github.com/mozilla-mobile/android-components/blob/main/components/feature/accounts/src/main/java/mozilla/components/feature/accounts/FxaWebChannelFeature.kt
|
||||
*/
|
||||
@Singleton
|
||||
class FrostCoreExtension
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val engine: Engine,
|
||||
private val store: BrowserStore,
|
||||
private val converter: ExtensionModelConverter,
|
||||
) {
|
||||
|
||||
private val extensionController =
|
||||
WebExtensionController(
|
||||
WEB_CHANNEL_EXTENSION_ID,
|
||||
WEB_CHANNEL_EXTENSION_URL,
|
||||
WEB_CHANNEL_MESSAGING_ID,
|
||||
)
|
||||
|
||||
fun install() {
|
||||
logger.atInfo().log("extension background start")
|
||||
val messageHandler = FrostBackgroundMessageHandler()
|
||||
extensionController.registerBackgroundMessageHandler(
|
||||
messageHandler,
|
||||
WEB_CHANNEL_BACKGROUND_MESSAGING_ID,
|
||||
)
|
||||
|
||||
extensionController.install(
|
||||
engine,
|
||||
onSuccess = {
|
||||
logger.atInfo().log("extension install success")
|
||||
extensionController.sendBackgroundMessage(
|
||||
JSONObject().apply { put("test", 0) },
|
||||
WEB_CHANNEL_BACKGROUND_MESSAGING_ID
|
||||
)
|
||||
},
|
||||
onError = { t -> logger.atWarning().withCause(t).log("extension install failure") },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun installContent(owner: LifecycleOwner? = null, customTabSessionId: String? = null) {
|
||||
logger.atInfo().log("extension content start")
|
||||
|
||||
store
|
||||
.flow(owner)
|
||||
.mapNotNull { state -> state.findCustomTabOrSelectedTab(customTabSessionId) }
|
||||
.ifChanged { it.engineState.engineSession }
|
||||
.collect {
|
||||
it.engineState.engineSession?.let { engineSession ->
|
||||
logger.atInfo().log("Register content message handler ${it.id}")
|
||||
registerContentMessageHandler(engineSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerContentMessageHandler(engineSession: EngineSession) {
|
||||
val messageHandler = FrostMessageHandler(converter)
|
||||
extensionController.registerContentMessageHandler(engineSession, messageHandler)
|
||||
}
|
||||
|
||||
private class FrostBackgroundMessageHandler : MessageHandler {
|
||||
|
||||
override fun onMessage(message: Any, source: EngineSession?): Any? {
|
||||
logger.atInfo().log("onMessage: %s", message)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onPortConnected(port: Port) {
|
||||
logger.atInfo().log("background onPortConnected: %s", port.name())
|
||||
}
|
||||
}
|
||||
|
||||
private class FrostMessageHandler(private val converter: ExtensionModelConverter) :
|
||||
MessageHandler {
|
||||
override fun onMessage(message: Any, source: EngineSession?): Any? {
|
||||
if (message is String) {
|
||||
logger.atInfo().log("onMessage: %s", message)
|
||||
return null
|
||||
}
|
||||
val model = converter.fromJSONObject(message as? JSONObject)
|
||||
if (model == null) {
|
||||
logger.atWarning().log("onMessage - unexpected format: %s", message)
|
||||
return null
|
||||
}
|
||||
logger.atFine().log("onMessage: %s", model)
|
||||
when (model) {
|
||||
is UrlClick -> {
|
||||
logger.atInfo().log("UrlClick ${model.url}")
|
||||
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
logger.atWarning().log("onMessage - unhandled: %s", model)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onPortConnected(port: Port) {
|
||||
logger.atInfo().log("content onPortConnected")
|
||||
super.onPortConnected(port)
|
||||
}
|
||||
|
||||
override fun onPortMessage(message: Any, port: Port) {
|
||||
if (message is String) {
|
||||
logger.atInfo().log("onPortMessage: %s", message)
|
||||
return
|
||||
}
|
||||
val model = converter.fromJSONObject(message as? JSONObject)
|
||||
if (model == null) {
|
||||
logger.atWarning().log("onPortMessage - unexpected format: %s", message)
|
||||
return
|
||||
}
|
||||
logger.atFine().log("onPortMessage: %s", model)
|
||||
// TODO
|
||||
}
|
||||
|
||||
private fun Port.postMessage(message: ExtensionModel) {
|
||||
val json = converter.toJSONObject(message)
|
||||
if (json == null) {
|
||||
logger.atSevere().log("postMessage - unexpected format: %s", message)
|
||||
return
|
||||
}
|
||||
postMessage(json)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
const val WEB_CHANNEL_EXTENSION_ID = "frost_gecko_core@pitchedapps"
|
||||
const val WEB_CHANNEL_MESSAGING_ID = "frostChannel"
|
||||
const val WEB_CHANNEL_BACKGROUND_MESSAGING_ID = "frostBackgroundChannel"
|
||||
const val WEB_CHANNEL_EXTENSION_URL = "resource://android/assets/frostcore/"
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
* 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.extension
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import org.json.JSONObject
|
||||
|
||||
/** Model for messages between Frost native and Frost web extension. */
|
||||
object ExtensionType {
|
||||
const val TEST = "type-test"
|
||||
const val URL_CLICK = "url-click"
|
||||
|
||||
fun moshiFactory(): JsonAdapter.Factory {
|
||||
return PolymorphicJsonAdapterFactory.of(ExtensionModel::class.java, "type")
|
||||
.withSubtype(TestModel::class.java, TEST)
|
||||
.withSubtype(UrlClick::class.java, URL_CLICK)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* kotlinx.serialization seems to support polymorphism:
|
||||
* https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md
|
||||
*
|
||||
* But it won't work for us since we receive data from JS without metadata. As a result, we will map
|
||||
* the decoding ourself.
|
||||
*/
|
||||
sealed interface ExtensionModel {
|
||||
companion object
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TestModel(val message: String = "Test Model Message") : ExtensionModel
|
||||
|
||||
@JsonClass(generateAdapter = true) data class UrlClick(val url: String) : ExtensionModel
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Singleton
|
||||
class ExtensionModelConverter @Inject internal constructor(moshi: Moshi) {
|
||||
private val jsonObjectAdapter = moshi.adapter<JSONObject>()
|
||||
private val modelAdapter = moshi.adapter<ExtensionModel>()
|
||||
|
||||
fun toJSONObject(value: ExtensionModel): JSONObject? {
|
||||
val jsonValue = modelAdapter.toJsonValue(value) ?: return null
|
||||
return jsonObjectAdapter.fromJsonValue(jsonValue)
|
||||
}
|
||||
|
||||
fun fromJSONObject(value: JSONObject?): ExtensionModel? {
|
||||
value ?: return null
|
||||
val jsonValue = jsonObjectAdapter.toJsonValue(value) ?: return null
|
||||
return modelAdapter.fromJsonValue(jsonValue)
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@ package com.pitchedapps.frost.hilt
|
||||
import com.pitchedapps.frost.components.Core
|
||||
import com.pitchedapps.frost.components.FrostDataStore
|
||||
import com.pitchedapps.frost.components.UseCases
|
||||
import com.pitchedapps.frost.extension.ExtensionModelConverter
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -36,6 +35,5 @@ class FrostComponents
|
||||
internal constructor(
|
||||
val core: Core,
|
||||
val useCases: UseCases,
|
||||
val extensionModelConverter: ExtensionModelConverter,
|
||||
val dataStore: FrostDataStore,
|
||||
)
|
||||
|
@ -1,173 +0,0 @@
|
||||
/*
|
||||
* 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.hilt
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.BuildConfig
|
||||
import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.components.usecases.HomeTabsUseCases
|
||||
import com.pitchedapps.frost.main.MainActivity
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.util.Optional
|
||||
import javax.inject.Singleton
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage
|
||||
import mozilla.components.browser.icons.BrowserIcons
|
||||
import mozilla.components.browser.session.storage.SessionStorage
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.EngineAction
|
||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.DefaultSettings
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.Settings
|
||||
import mozilla.components.concept.engine.permission.SitePermissionsStorage
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.feature.prompts.PromptMiddleware
|
||||
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
||||
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.support.base.android.NotificationsDelegate
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FrostGeckoModule {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun geckoRuntime(@ApplicationContext context: Context): GeckoRuntime {
|
||||
val settings =
|
||||
GeckoRuntimeSettings.Builder()
|
||||
.consoleOutput(BuildConfig.DEBUG)
|
||||
.loginAutofillEnabled(true)
|
||||
// .debugLogging(false)
|
||||
.debugLogging(BuildConfig.DEBUG)
|
||||
.javaScriptEnabled(true)
|
||||
.build()
|
||||
|
||||
return GeckoRuntime.create(context, settings)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun client(@ApplicationContext context: Context, runtime: GeckoRuntime): Client {
|
||||
return GeckoViewFetchClient(context, runtime)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun settings(@Frost userAgent: Optional<String>): Settings {
|
||||
return DefaultSettings(userAgentString = userAgent.getOrNull())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun engine(
|
||||
@ApplicationContext context: Context,
|
||||
settings: Settings,
|
||||
runtime: GeckoRuntime
|
||||
): Engine {
|
||||
return GeckoEngine(context, settings, runtime)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun browserIcons(@ApplicationContext context: Context, client: Client): BrowserIcons {
|
||||
return BrowserIcons(context, client)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun sitePermissionStorage(
|
||||
@ApplicationContext context: Context,
|
||||
runtime: GeckoRuntime
|
||||
): SitePermissionsStorage {
|
||||
return GeckoSitePermissionsStorage(runtime, OnDiskSitePermissionsStorage(context))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun sessionStorage(@ApplicationContext context: Context, engine: Engine): SessionStorage {
|
||||
return SessionStorage(context, engine)
|
||||
}
|
||||
|
||||
private class LoggerMiddleWare : Middleware<BrowserState, BrowserAction> {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<BrowserState, BrowserAction>,
|
||||
next: (BrowserAction) -> Unit,
|
||||
action: BrowserAction
|
||||
) {
|
||||
if (action is EngineAction.LoadUrlAction) {
|
||||
logger.atInfo().log("BrowserAction: LoadUrlAction %s", action.url)
|
||||
} else {
|
||||
logger.atInfo().log("BrowserAction: %s - %s", action::class.simpleName, action)
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun browserStore(
|
||||
@ApplicationContext context: Context,
|
||||
icons: BrowserIcons,
|
||||
sitePermissionsStorage: SitePermissionsStorage,
|
||||
engine: Engine,
|
||||
notificationsDelegate: NotificationsDelegate,
|
||||
): BrowserStore {
|
||||
|
||||
val middleware = buildList {
|
||||
if (BuildConfig.DEBUG) add(LoggerMiddleWare())
|
||||
add(HomeTabsUseCases.HomeMiddleware())
|
||||
add(PromptMiddleware())
|
||||
// add(DownloadMiddleware(context, DownloadService::class.java))
|
||||
addAll(EngineMiddleware.create(engine))
|
||||
}
|
||||
|
||||
val store = BrowserStore(middleware = middleware)
|
||||
icons.install(engine, store)
|
||||
WebNotificationFeature(
|
||||
context = context,
|
||||
engine = engine,
|
||||
browserIcons = icons,
|
||||
smallIcon = R.mipmap.ic_launcher_round,
|
||||
sitePermissionsStorage = sitePermissionsStorage,
|
||||
activityClass = MainActivity::class.java,
|
||||
notificationsDelegate = notificationsDelegate,
|
||||
)
|
||||
return store
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun notificationDelegate(@ApplicationContext context: Context): NotificationsDelegate {
|
||||
return NotificationsDelegate(NotificationManagerCompat.from(context))
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.hilt
|
||||
|
||||
import com.pitchedapps.frost.extension.ExtensionType
|
||||
import com.squareup.moshi.*
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dagger.Module
|
||||
@ -37,7 +36,7 @@ object MoshiModule {
|
||||
@Singleton
|
||||
fun moshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.add(ExtensionType.moshiFactory())
|
||||
// .add(ExtensionType.moshiFactory())
|
||||
.add(JSONObjectAdapter())
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
@ -1,163 +0,0 @@
|
||||
/*
|
||||
* 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.main
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.pitchedapps.frost.compose.FrostWeb
|
||||
import com.pitchedapps.frost.ext.GeckoContextId
|
||||
import com.pitchedapps.frost.ext.components
|
||||
import mozilla.components.browser.state.helper.Target
|
||||
|
||||
/**
|
||||
* Screen for MainActivity.
|
||||
*
|
||||
* Note that [tabs] are passed in without listening to a datastore as they should be effectively
|
||||
* fixed. We don't want to update tabs as soon as users change the tabs in settingss.
|
||||
*/
|
||||
@Composable
|
||||
fun MainScreen(modifier: Modifier = Modifier, tabs: List<MainTabItem>) {
|
||||
val vm: MainScreenViewModel = viewModel()
|
||||
|
||||
if (tabs.isEmpty()) return // not ready
|
||||
|
||||
// val contextId = GeckoContextId("test-context")
|
||||
val contextId = vm.contextIdFlow.collectAsState(initial = null).value ?: return // not ready
|
||||
|
||||
LaunchedEffect(vm) { vm.frostCoreExtension.installContent() }
|
||||
|
||||
val onTabSelect =
|
||||
remember(vm) {
|
||||
{ selectedIndex: Int ->
|
||||
if (selectedIndex == vm.tabIndex) {
|
||||
vm.components.useCases.homeTabs.reloadTab(selectedIndex)
|
||||
// context.launchFloatingUrl(FACEBOOK_M_URL)
|
||||
} else {
|
||||
// Change? What if previous selected tab is not home tab
|
||||
vm.components.useCases.homeTabs.selectHomeTab(selectedIndex)
|
||||
vm.tabIndex = selectedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MainContainer(
|
||||
modifier = modifier,
|
||||
contextId = contextId,
|
||||
tabIndex = vm.tabIndex,
|
||||
tabs = tabs,
|
||||
onTabSelect = onTabSelect,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainContainer(
|
||||
contextId: GeckoContextId,
|
||||
tabIndex: Int,
|
||||
tabs: List<MainTabItem>,
|
||||
onTabSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val components = LocalContext.current.components
|
||||
|
||||
LaunchedEffect(contextId, tabs) {
|
||||
components.useCases.homeTabs.createHomeTabs(contextId, tabIndex, tabs.map { it.url })
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
MainHeader(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
title = tabs.getOrNull(tabIndex)?.title ?: "",
|
||||
)
|
||||
if (tabs.size > 1) {
|
||||
MainTabRow(
|
||||
selectedIndex = tabIndex,
|
||||
items = tabs,
|
||||
onTabSelect = onTabSelect,
|
||||
)
|
||||
}
|
||||
// For tab switching, must use SelectedTab
|
||||
// https://github.com/mozilla-mobile/android-components/issues/12798
|
||||
FrostWeb(
|
||||
engine = components.core.engine,
|
||||
store = components.core.store,
|
||||
target = Target.SelectedTab,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainHeader(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = { Text(text = title) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainTabRow(
|
||||
selectedIndex: Int,
|
||||
items: List<MainTabItem>,
|
||||
onTabSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TabRow(modifier = modifier, selectedTabIndex = selectedIndex, indicator = {}) {
|
||||
items.forEachIndexed { i, item ->
|
||||
val selected = selectedIndex == i
|
||||
Tab(selected = selected, onClick = { onTabSelect(i) }) {
|
||||
MainTabItem(modifier = Modifier.padding(16.dp), item = item, selected = selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainTabItem(item: MainTabItem, selected: Boolean, modifier: Modifier = Modifier) {
|
||||
val alpha by
|
||||
animateFloatAsState(
|
||||
targetValue = if (selected) 1f else ContentAlpha.medium,
|
||||
label = "Tab Alpha",
|
||||
)
|
||||
Icon(
|
||||
modifier = modifier.alpha(alpha).size(24.dp),
|
||||
contentDescription = item.title,
|
||||
imageVector = item.icon,
|
||||
)
|
||||
}
|
@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModel
|
||||
import com.pitchedapps.frost.ext.GeckoContextId
|
||||
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
|
||||
@ -40,7 +39,6 @@ class MainScreenViewModel
|
||||
internal constructor(
|
||||
@ApplicationContext context: Context,
|
||||
val components: FrostComponents,
|
||||
val frostCoreExtension: FrostCoreExtension,
|
||||
val store: FrostWebStore,
|
||||
val frostWebComposer: FrostWebComposer,
|
||||
// sample: FrostWebEntrySample,
|
||||
|
Loading…
Reference in New Issue
Block a user