mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-11-09 12:32:30 +01:00
Convert FrostWebStore into singleton
This commit is contained in:
parent
2794a104e8
commit
3bd266feb4
@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Kotlin2JvmCompilerArguments">
|
||||
<option name="jvmTarget" value="17" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.21" />
|
||||
</component>
|
||||
|
@ -156,7 +156,7 @@ dependencies {
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
|
||||
|
||||
def hilt = "2.43.2"
|
||||
def hilt = "2.46.1"
|
||||
implementation "com.google.dagger:hilt-android:${hilt}"
|
||||
kapt "com.google.dagger:hilt-android-compiler:${hilt}"
|
||||
|
||||
|
@ -21,15 +21,25 @@ import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.UpdateProgressAction
|
||||
import com.pitchedapps.frost.web.state.UpdateTitleAction
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateProgressAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateTitleAction
|
||||
import com.pitchedapps.frost.web.state.get
|
||||
import com.pitchedapps.frost.webview.FrostWeb
|
||||
import javax.inject.Inject
|
||||
|
||||
/** The default chrome client */
|
||||
class FrostChromeClient @Inject internal constructor(private val store: FrostWebStore) :
|
||||
class FrostChromeClient
|
||||
@Inject
|
||||
internal constructor(@FrostWeb private val tabId: WebTargetId, private val store: FrostWebStore) :
|
||||
WebChromeClient() {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
override fun getDefaultVideoPoster(): Bitmap? =
|
||||
super.getDefaultVideoPoster() ?: Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
|
||||
|
||||
@ -48,7 +58,8 @@ class FrostChromeClient @Inject internal constructor(private val store: FrostWeb
|
||||
|
||||
override fun onProgressChanged(view: WebView, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
if (store.state.progress == 100) return
|
||||
// TODO remove?
|
||||
if (store.state[tabId]?.progress == 100) return
|
||||
store.dispatch(UpdateProgressAction(newProgress))
|
||||
}
|
||||
|
||||
|
@ -30,11 +30,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.children
|
||||
import com.pitchedapps.frost.web.state.FrostWebState
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.ResponseAction
|
||||
import com.pitchedapps.frost.webview.FrostWebScoped
|
||||
import javax.inject.Inject
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.WebStepResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabWebState
|
||||
import com.pitchedapps.frost.web.state.get
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
@ -43,15 +46,17 @@ import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.ext.flow
|
||||
import mozilla.components.lib.state.ext.observeAsState
|
||||
|
||||
@FrostWebScoped
|
||||
class FrostWebCompose
|
||||
@Inject
|
||||
internal constructor(
|
||||
class FrostWebCompose(
|
||||
val tabId: WebTargetId,
|
||||
private val store: FrostWebStore,
|
||||
private val client: FrostWebViewClient,
|
||||
private val chromeClient: FrostChromeClient,
|
||||
) {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
/**
|
||||
* Webview implementation in compose
|
||||
*
|
||||
@ -96,20 +101,20 @@ internal constructor(
|
||||
webView?.let { wv ->
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val canGoBack by store.observeAsState(initialValue = false) { it.canGoBack }
|
||||
val canGoBack by store.observeAsState(initialValue = false) { it[tabId]?.canGoBack == true }
|
||||
|
||||
BackHandler(captureBackPresses && canGoBack) { wv.goBack() }
|
||||
|
||||
LaunchedEffect(wv, store) {
|
||||
fun storeFlow(action: suspend Flow<FrostWebState>.() -> Unit) = launch {
|
||||
store.flow(lifecycleOwner).action()
|
||||
fun storeFlow(action: suspend Flow<TabWebState>.() -> Unit) = launch {
|
||||
store.flow(lifecycleOwner).mapNotNull { it[tabId] }.action()
|
||||
}
|
||||
|
||||
storeFlow {
|
||||
mapNotNull { it.transientState.targetUrl }
|
||||
.distinctUntilChanged()
|
||||
.collect { url ->
|
||||
store.dispatch(ResponseAction.LoadUrlResponseAction(url))
|
||||
store.dispatch(LoadUrlResponseAction(url))
|
||||
wv.loadUrl(url)
|
||||
}
|
||||
}
|
||||
@ -118,9 +123,11 @@ internal constructor(
|
||||
.distinctUntilChanged()
|
||||
.filter { it != 0 }
|
||||
.collect { steps ->
|
||||
store.dispatch(ResponseAction.WebStepResponseAction(steps))
|
||||
store.dispatch(WebStepResponseAction(steps))
|
||||
if (wv.canGoBackOrForward(steps)) {
|
||||
wv.goBackOrForward(steps)
|
||||
} else {
|
||||
logger.atWarning().log("web %s cannot go back %d steps", tabId, steps)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -169,6 +176,10 @@ internal constructor(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -22,14 +22,17 @@ import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.facebook.FACEBOOK_BASE_COM
|
||||
import com.pitchedapps.frost.facebook.WWW_FACEBOOK_COM
|
||||
import com.pitchedapps.frost.facebook.isExplicitIntent
|
||||
import com.pitchedapps.frost.web.FrostWebHelper
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.UpdateNavigationAction
|
||||
import com.pitchedapps.frost.web.state.UpdateProgressAction
|
||||
import com.pitchedapps.frost.web.state.UpdateTitleAction
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateNavigationAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateProgressAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateTitleAction
|
||||
import com.pitchedapps.frost.webview.FrostWeb
|
||||
import java.io.ByteArrayInputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -61,8 +64,15 @@ abstract class BaseWebViewClient : WebViewClient() {
|
||||
/** The default webview client */
|
||||
class FrostWebViewClient
|
||||
@Inject
|
||||
internal constructor(private val store: FrostWebStore, override val webHelper: FrostWebHelper) :
|
||||
BaseWebViewClient() {
|
||||
internal constructor(
|
||||
@FrostWeb private val tabId: WebTargetId,
|
||||
private val store: FrostWebStore,
|
||||
override val webHelper: FrostWebHelper
|
||||
) : BaseWebViewClient() {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
/** True if current url supports refresh. See [doUpdateVisitedHistory] for updates */
|
||||
internal var urlSupportsRefresh: Boolean = true
|
||||
|
@ -28,6 +28,8 @@ import kotlinx.coroutines.flow.map
|
||||
*/
|
||||
@JvmInline value class FrostAccountId(val id: Long)
|
||||
|
||||
@JvmInline value class WebTargetId(val id: String)
|
||||
|
||||
/**
|
||||
* Representation of gecko context id.
|
||||
*
|
||||
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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,47 +16,15 @@
|
||||
*/
|
||||
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 com.pitchedapps.frost.web.FrostAdBlock
|
||||
import dagger.BindsOptionalOf
|
||||
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.Qualifier
|
||||
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
|
||||
|
||||
@Qualifier annotation class Frost
|
||||
|
||||
@ -68,7 +36,7 @@ interface FrostBindModule {
|
||||
@BindsOptionalOf fun adBlock(): FrostAdBlock
|
||||
}
|
||||
|
||||
/** Module containing core Mozilla injections. */
|
||||
/** Module containing core Frost injections. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FrostModule {
|
||||
@ -89,115 +57,4 @@ object FrostModule {
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Safari/537.36"
|
||||
|
||||
@Provides @Singleton @Frost fun userAgent(): String = USER_AGENT_WINDOWS_FROST
|
||||
|
||||
@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))
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.webkit.CookieManager
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.BuildConfig
|
||||
import com.pitchedapps.frost.web.state.FrostLoggerMiddleware
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Module containing core WebView injections. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FrostWebViewModule {
|
||||
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
@Provides @Singleton fun cookieManager(): CookieManager = CookieManager.getInstance()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun frostWebStore(): FrostWebStore {
|
||||
val middleware = buildList { if (BuildConfig.DEBUG) add(FrostLoggerMiddleware()) }
|
||||
|
||||
val store = FrostWebStore(middleware = middleware)
|
||||
|
||||
return store
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ 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
|
||||
@ -49,5 +50,5 @@ internal constructor(
|
||||
|
||||
var tabIndex: Int by mutableStateOf(0)
|
||||
|
||||
val frostWebCompose: FrostWebCompose = frostWebComposer.create("test")
|
||||
val frostWebCompose: FrostWebCompose = frostWebComposer.create(WebTargetId("test"))
|
||||
}
|
||||
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.CookieManager
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.facebook.FACEBOOK_COM
|
||||
import com.pitchedapps.frost.facebook.HTTPS_FACEBOOK_COM
|
||||
import com.pitchedapps.frost.facebook.HTTPS_MESSENGER_COM
|
||||
import com.pitchedapps.frost.facebook.MESSENGER_COM
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FrostCookie @Inject internal constructor(private val cookieManager: CookieManager) {
|
||||
|
||||
val fbCookie: String?
|
||||
get() = cookieManager.getCookie(HTTPS_FACEBOOK_COM)
|
||||
|
||||
val messengerCookie: String?
|
||||
get() = cookieManager.getCookie(HTTPS_MESSENGER_COM)
|
||||
|
||||
private suspend fun CookieManager.suspendSetWebCookie(domain: String, cookie: String?): Boolean {
|
||||
cookie ?: return true
|
||||
return withContext(NonCancellable) {
|
||||
// Save all cookies regardless of result, then check if all succeeded
|
||||
val result =
|
||||
cookie.split(";").map { async { setSingleWebCookie(domain, it) } }.awaitAll().all { it }
|
||||
logger.atInfo().log("Cookies set for %s, %b", domain, result)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.setSingleWebCookie(domain: String, cookie: String): Boolean =
|
||||
suspendCoroutine { cont ->
|
||||
setCookie(domain, cookie.trim()) { cont.resume(it) }
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
||||
removeAllCookies { cont.resume(it) }
|
||||
}
|
||||
|
||||
suspend fun save(id: Long) {
|
||||
logger.atInfo().log("Saving cookies for %d", id)
|
||||
// prefs.userId = id
|
||||
cookieManager.flush()
|
||||
// val cookie = CookieEntity(prefs.userId, null, webCookie)
|
||||
// cookieDao.save(cookie)
|
||||
}
|
||||
|
||||
suspend fun reset() {
|
||||
// prefs.userId = -1L
|
||||
withContext(Dispatchers.Main + NonCancellable) {
|
||||
with(cookieManager) {
|
||||
removeAllCookies()
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun switchUser(id: Long) {
|
||||
// val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
|
||||
// switchUser(cookie)
|
||||
}
|
||||
|
||||
// suspend fun switchUser(cookie: CookieEntity?) {
|
||||
// if (cookie?.cookie == null) {
|
||||
// L.d { "Switching User; null cookie" }
|
||||
// return
|
||||
// }
|
||||
// withContext(Dispatchers.Main + NonCancellable) {
|
||||
// L.d { "Switching User" }
|
||||
// prefs.userId = cookie.id
|
||||
// CookieManager.getInstance().apply {
|
||||
// removeAllCookies()
|
||||
// suspendSetWebCookie(FB_COOKIE_DOMAIN, cookie.cookie)
|
||||
// suspendSetWebCookie(MESSENGER_COOKIE_DOMAIN, cookie.cookieMessenger)
|
||||
// flush()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/** Helper function to remove the current cookies and launch the proper login page */
|
||||
suspend fun logout(context: Context, deleteCookie: Boolean = true) {
|
||||
// val cookies = arrayListOf<CookieEntity>()
|
||||
// if (context is Activity) cookies.addAll(context.cookies().filter { it.id != prefs.userId
|
||||
// })
|
||||
// logout(prefs.userId, deleteCookie)
|
||||
// context.launchLogin(cookies, true)
|
||||
}
|
||||
|
||||
/** Clear the cookies of the given id */
|
||||
suspend fun logout(id: Long, deleteCookie: Boolean = true) {
|
||||
logger.atInfo().log("Logging out user %d", id)
|
||||
// TODO save cookies?
|
||||
if (deleteCookie) {
|
||||
// cookieDao.deleteById(id)
|
||||
logger.atInfo().log("Deleted cookies")
|
||||
}
|
||||
reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifications may come from different accounts, and we need to switch the cookies to load them
|
||||
* When coming back to the main app, switch back to our original account before continuing
|
||||
*/
|
||||
suspend fun switchBackUser() {
|
||||
// if (prefs.prevId == -1L) return
|
||||
// val prevId = prefs.prevId
|
||||
// prefs.prevId = -1L
|
||||
// if (prevId != prefs.userId) {
|
||||
// switchUser(prevId)
|
||||
// L.d { "Switch back user" }
|
||||
// L._d { "${prefs.userId} to $prevId" }
|
||||
// }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
/** Domain information. Dot prefix still matters for Android browsers. */
|
||||
private const val FB_COOKIE_DOMAIN = ".$FACEBOOK_COM"
|
||||
private const val MESSENGER_COOKIE_DOMAIN = ".$MESSENGER_COM"
|
||||
}
|
||||
}
|
@ -20,13 +20,15 @@ import com.google.common.flogger.FluentLogger
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
|
||||
class FrostLoggerMiddleware(private val tag: String) : Middleware<FrostWebState, FrostWebAction> {
|
||||
typealias FrostWebMiddleware = Middleware<FrostWebState, FrostWebAction>
|
||||
|
||||
class FrostLoggerMiddleware : FrostWebMiddleware {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<FrostWebState, FrostWebAction>,
|
||||
next: (FrostWebAction) -> Unit,
|
||||
action: FrostWebAction
|
||||
) {
|
||||
logger.atInfo().log("FrostWebAction-%s: %s - %s", tag, action::class.simpleName, action)
|
||||
logger.atInfo().log("FrostWebAction: %s - %s", action::class.simpleName, action)
|
||||
next(action)
|
||||
}
|
||||
|
||||
@ -34,3 +36,15 @@ class FrostLoggerMiddleware(private val tag: String) : Middleware<FrostWebState,
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
||||
|
||||
class FrostCookieMiddleware : FrostWebMiddleware {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<FrostWebState, FrostWebAction>,
|
||||
next: (FrostWebAction) -> Unit,
|
||||
action: FrostWebAction
|
||||
) {
|
||||
when (action) {
|
||||
else -> next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.web.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import mozilla.components.lib.state.Action
|
||||
|
||||
/**
|
||||
@ -34,19 +35,26 @@ sealed interface FrostWebAction : Action
|
||||
*/
|
||||
object InitAction : FrostWebAction
|
||||
|
||||
/** Action indicating current url state. */
|
||||
data class UpdateUrlAction(val url: String) : FrostWebAction
|
||||
/** Action affecting a single tab */
|
||||
data class TabAction(val tabId: WebTargetId, val action: Action) : FrostWebAction {
|
||||
sealed interface Action
|
||||
|
||||
/** Action indicating current title state. */
|
||||
data class UpdateTitleAction(val title: String?) : FrostWebAction
|
||||
sealed interface ContentAction : Action {
|
||||
|
||||
data class UpdateNavigationAction(val canGoBack: Boolean, val canGoForward: Boolean) :
|
||||
FrostWebAction
|
||||
/** Action indicating current url state. */
|
||||
data class UpdateUrlAction(val url: String) : ContentAction
|
||||
|
||||
data class UpdateProgressAction(val progress: Int) : FrostWebAction
|
||||
/** Action indicating current title state. */
|
||||
data class UpdateTitleAction(val title: String?) : ContentAction
|
||||
|
||||
/** Action triggered by user, leading to transient state changes. */
|
||||
sealed interface UserAction : FrostWebAction {
|
||||
data class UpdateNavigationAction(val canGoBack: Boolean, val canGoForward: Boolean) :
|
||||
ContentAction
|
||||
|
||||
data class UpdateProgressAction(val progress: Int) : ContentAction
|
||||
}
|
||||
|
||||
/** Action triggered by user, leading to transient state changes. */
|
||||
sealed interface UserAction : Action {
|
||||
|
||||
/** Action to load new url. */
|
||||
data class LoadUrlAction(val url: String) : UserAction
|
||||
@ -54,12 +62,13 @@ sealed interface UserAction : FrostWebAction {
|
||||
object GoBackAction : UserAction
|
||||
|
||||
object GoForwardAction : UserAction
|
||||
}
|
||||
}
|
||||
|
||||
/** Response triggered by webview, indicating [UserAction] fulfillment. */
|
||||
sealed interface ResponseAction : FrostWebAction {
|
||||
/** Response triggered by webview, indicating [UserAction] fulfillment. */
|
||||
sealed interface ResponseAction : Action {
|
||||
|
||||
data class LoadUrlResponseAction(val url: String) : ResponseAction
|
||||
|
||||
data class WebStepResponseAction(val steps: Int) : ResponseAction
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,9 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.web.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.reducer.ContentStateReducer
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt
|
||||
@ -26,50 +29,36 @@ internal object FrostWebReducer {
|
||||
fun reduce(state: FrostWebState, action: FrostWebAction): FrostWebState {
|
||||
return when (action) {
|
||||
is InitAction -> state
|
||||
is UpdateUrlAction -> state.copy(url = action.url)
|
||||
is UpdateProgressAction -> state.copy(progress = action.progress)
|
||||
is UpdateNavigationAction ->
|
||||
state.copy(
|
||||
canGoBack = action.canGoBack,
|
||||
canGoForward = action.canGoForward,
|
||||
)
|
||||
is UpdateTitleAction -> state.copy(title = action.title)
|
||||
is UserAction ->
|
||||
state.copy(
|
||||
transientState =
|
||||
FrostTransientWebReducer.reduce(
|
||||
state.transientState,
|
||||
action,
|
||||
),
|
||||
)
|
||||
is ResponseAction ->
|
||||
state.copy(
|
||||
transientState =
|
||||
FrostTransientFulfillmentWebReducer.reduce(
|
||||
state.transientState,
|
||||
action,
|
||||
),
|
||||
)
|
||||
is TabAction ->
|
||||
state.updateTabState(action.tabId) { ContentStateReducer.reduce(it, action.action) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object FrostTransientWebReducer {
|
||||
fun reduce(state: TransientWebState, action: UserAction): TransientWebState {
|
||||
return when (action) {
|
||||
is UserAction.LoadUrlAction -> state.copy(targetUrl = action.url)
|
||||
is UserAction.GoBackAction -> state.copy(navStep = state.navStep - 1)
|
||||
is UserAction.GoForwardAction -> state.copy(navStep = state.navStep + 1)
|
||||
}
|
||||
}
|
||||
internal fun FrostWebState.updateTabState(
|
||||
tabId: WebTargetId,
|
||||
update: (TabWebState) -> TabWebState,
|
||||
): FrostWebState {
|
||||
val floatingTabMatch = floatingTab?.takeIf { it.id == tabId }
|
||||
if (floatingTabMatch != null) return copy(floatingTab = update(floatingTabMatch))
|
||||
|
||||
val newHomeTabs = homeTabs.updateTabs(tabId, update)
|
||||
if (newHomeTabs != null) return copy(homeTabs = newHomeTabs)
|
||||
return this
|
||||
}
|
||||
|
||||
private object FrostTransientFulfillmentWebReducer {
|
||||
fun reduce(state: TransientWebState, action: ResponseAction): TransientWebState {
|
||||
return when (action) {
|
||||
is ResponseAction.LoadUrlResponseAction ->
|
||||
if (state.targetUrl == action.url) state.copy(targetUrl = null) else state
|
||||
is ResponseAction.WebStepResponseAction -> state.copy(navStep = state.navStep - action.steps)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Finds the corresponding tab in the list and replaces it using [update].
|
||||
*
|
||||
* @param tabId ID of the tab to change.
|
||||
* @param update Returns a new version of the tab state.
|
||||
*/
|
||||
internal fun List<TabWebState>.updateTabs(
|
||||
tabId: WebTargetId,
|
||||
update: (TabWebState) -> TabWebState,
|
||||
): List<TabWebState>? {
|
||||
val tabIndex = indexOfFirst { it.id == tabId }
|
||||
if (tabIndex == -1) return null
|
||||
|
||||
return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size)
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.web.state
|
||||
|
||||
import com.pitchedapps.frost.ext.FrostAccountId
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import mozilla.components.lib.state.State
|
||||
|
||||
/**
|
||||
@ -25,6 +27,39 @@ import mozilla.components.lib.state.State
|
||||
* for Firefox example.
|
||||
*/
|
||||
data class FrostWebState(
|
||||
val auth: AuthWebState = AuthWebState(),
|
||||
val homeTabs: List<TabWebState> = emptyList(),
|
||||
var floatingTab: TabWebState? = null,
|
||||
) : State
|
||||
|
||||
/**
|
||||
* Auth web state.
|
||||
*
|
||||
* Unlike GeckoView, WebView currently has a singleton cookie manager.
|
||||
*
|
||||
* Cookies are tied to the entire app, rather than per tab.
|
||||
*
|
||||
* @param currentUser User based on loaded cookies
|
||||
* @param homeUser User selected for home screen
|
||||
*/
|
||||
data class AuthWebState(
|
||||
val currentUser: AuthUser = AuthUser.Unknown,
|
||||
val homeUser: AuthUser = AuthUser.Unknown,
|
||||
) {
|
||||
sealed interface AuthUser {
|
||||
data class User(val id: FrostAccountId) : AuthUser
|
||||
|
||||
data class Transitioning(val targetId: FrostAccountId?) : AuthUser
|
||||
|
||||
object LoggedOut : AuthUser
|
||||
|
||||
object Unknown : AuthUser
|
||||
}
|
||||
}
|
||||
|
||||
data class TabWebState(
|
||||
val id: WebTargetId,
|
||||
val userId: AuthWebState.AuthUser,
|
||||
val baseUrl: String? = null,
|
||||
val url: String? = null,
|
||||
val title: String? = null,
|
||||
@ -32,7 +67,7 @@ data class FrostWebState(
|
||||
val canGoBack: Boolean = false,
|
||||
val canGoForward: Boolean = false,
|
||||
val transientState: TransientWebState = TransientWebState(),
|
||||
) : State
|
||||
)
|
||||
|
||||
/**
|
||||
* Transient web state.
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.web.state
|
||||
|
||||
import com.pitchedapps.frost.facebook.FB_URL_BASE
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
@ -27,7 +27,6 @@ import mozilla.components.lib.state.Store
|
||||
* For firefox example.
|
||||
*/
|
||||
class FrostWebStore(
|
||||
tag: String,
|
||||
initialState: FrostWebState = FrostWebState(),
|
||||
middleware: List<Middleware<FrostWebState, FrostWebAction>> = emptyList(),
|
||||
) :
|
||||
@ -35,10 +34,14 @@ class FrostWebStore(
|
||||
initialState,
|
||||
FrostWebReducer::reduce,
|
||||
middleware,
|
||||
"FrostStore-$tag",
|
||||
"FrostStore",
|
||||
) {
|
||||
init {
|
||||
dispatch(InitAction)
|
||||
dispatch(UserAction.LoadUrlAction(FB_URL_BASE))
|
||||
}
|
||||
}
|
||||
|
||||
operator fun FrostWebState.get(tabId: WebTargetId): TabWebState? {
|
||||
if (floatingTab?.id == tabId) return floatingTab
|
||||
return homeTabs.find { it.id == tabId }
|
||||
}
|
||||
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.helper
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.FrostWebState
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabWebState
|
||||
import mozilla.components.lib.state.Store
|
||||
import mozilla.components.lib.state.ext.observeAsComposableState
|
||||
|
||||
/**
|
||||
* Helper for allowing a component consumer to specify which tab a component should target.
|
||||
*
|
||||
* Based off of mozilla.components.browser.state.helper.Target:
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/helper/Target.kt
|
||||
*/
|
||||
sealed class Target {
|
||||
/**
|
||||
* Looks up this target in the given [FrostWebStore] and returns the matching [TabWebState] if
|
||||
* available. Otherwise returns `null`.
|
||||
*
|
||||
* @param store to lookup this target in.
|
||||
*/
|
||||
fun lookupIn(store: FrostWebStore): TabWebState? = lookupIn(store.state)
|
||||
|
||||
/**
|
||||
* Looks up this target in the given [FrostWebState] and returns the matching [TabWebState] if
|
||||
* available. Otherwise returns `null`.
|
||||
*
|
||||
* @param state to lookup this target in.
|
||||
*/
|
||||
abstract fun lookupIn(state: FrostWebState): TabWebState?
|
||||
|
||||
/**
|
||||
* Observes this target and represents the mapped state (using [map]) via [State].
|
||||
*
|
||||
* Everytime the [Store] state changes and the result of the [observe] function changes for this
|
||||
* state, the returned [State] will be updated causing recomposition of every [State.value] usage.
|
||||
*
|
||||
* The [Store] observer will automatically be removed when this composable disposes or the current
|
||||
* [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
|
||||
*
|
||||
* @param store that should get observed
|
||||
* @param observe function that maps a [TabWebState] to the (sub) state that should get observed
|
||||
* for changes.
|
||||
*/
|
||||
@Composable
|
||||
fun <R> observeAsComposableStateFrom(
|
||||
store: FrostWebStore,
|
||||
observe: (TabWebState?) -> R,
|
||||
): State<TabWebState?> {
|
||||
return store.observeAsComposableState(
|
||||
map = { state -> lookupIn(state) },
|
||||
observe = { state -> observe(lookupIn(state)) },
|
||||
)
|
||||
}
|
||||
|
||||
data class HomeTab(val id: WebTargetId) : Target() {
|
||||
override fun lookupIn(state: FrostWebState): TabWebState? {
|
||||
return state.homeTabs.find { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
object FloatingTab : Target() {
|
||||
override fun lookupIn(state: FrostWebState): TabWebState? {
|
||||
return state.floatingTab
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.reducer
|
||||
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.Action
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateNavigationAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateProgressAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateTitleAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateUrlAction
|
||||
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.UserAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.GoBackAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.GoForwardAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.LoadUrlAction
|
||||
import com.pitchedapps.frost.web.state.TabWebState
|
||||
import com.pitchedapps.frost.web.state.TransientWebState
|
||||
|
||||
internal object ContentStateReducer {
|
||||
fun reduce(state: TabWebState, action: Action): TabWebState {
|
||||
return when (action) {
|
||||
is UpdateUrlAction -> state.copy(url = action.url)
|
||||
is UpdateProgressAction -> state.copy(progress = action.progress)
|
||||
is UpdateNavigationAction ->
|
||||
state.copy(
|
||||
canGoBack = action.canGoBack,
|
||||
canGoForward = action.canGoForward,
|
||||
)
|
||||
is UpdateTitleAction -> state.copy(title = action.title)
|
||||
is TabAction.UserAction ->
|
||||
state.copy(
|
||||
transientState =
|
||||
FrostTransientWebReducer.reduce(
|
||||
state.transientState,
|
||||
action,
|
||||
),
|
||||
)
|
||||
is TabAction.ResponseAction ->
|
||||
state.copy(
|
||||
transientState =
|
||||
FrostTransientFulfillmentWebReducer.reduce(
|
||||
state.transientState,
|
||||
action,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object FrostTransientWebReducer {
|
||||
fun reduce(state: TransientWebState, action: UserAction): TransientWebState {
|
||||
return when (action) {
|
||||
is LoadUrlAction -> state.copy(targetUrl = action.url)
|
||||
is GoBackAction -> state.copy(navStep = state.navStep - 1)
|
||||
is GoForwardAction -> state.copy(navStep = state.navStep + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object FrostTransientFulfillmentWebReducer {
|
||||
fun reduce(state: TransientWebState, action: TabAction.ResponseAction): TransientWebState {
|
||||
return when (action) {
|
||||
is LoadUrlResponseAction ->
|
||||
if (state.targetUrl == action.url) state.copy(targetUrl = null) else state
|
||||
is WebStepResponseAction -> state.copy(navStep = state.navStep - action.steps)
|
||||
}
|
||||
}
|
||||
}
|
@ -16,18 +16,10 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.webview
|
||||
|
||||
import com.pitchedapps.frost.compose.webview.FrostWebCompose
|
||||
import com.pitchedapps.frost.web.state.FrostLoggerMiddleware
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import dagger.BindsInstance
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.DefineComponent
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.EntryPoints
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Scope
|
||||
|
||||
@ -45,38 +37,11 @@ annotation class FrostWebScoped
|
||||
|
||||
@FrostWebScoped @DefineComponent(parent = ViewModelComponent::class) interface FrostWebComponent
|
||||
|
||||
/** Using this component seems to be buggy, leading to an invalid param tabId error. */
|
||||
@DefineComponent.Builder
|
||||
interface FrostWebComponentBuilder {
|
||||
fun id(@BindsInstance @FrostWeb id: String): FrostWebComponentBuilder
|
||||
|
||||
@BindsInstance fun tabId(@FrostWeb tabId: WebTargetId): FrostWebComponentBuilder
|
||||
|
||||
fun build(): FrostWebComponent
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(FrostWebComponent::class)
|
||||
internal object FrostWebModule {
|
||||
|
||||
@Provides
|
||||
@FrostWebScoped
|
||||
fun frostStore(@FrostWeb id: String): FrostWebStore {
|
||||
val logger = FrostLoggerMiddleware(id)
|
||||
|
||||
return FrostWebStore(tag = id, middleware = listOf(logger))
|
||||
}
|
||||
}
|
||||
|
||||
class FrostWebComposer
|
||||
@Inject
|
||||
internal constructor(private val frostWebComponentBuilder: FrostWebComponentBuilder) {
|
||||
fun create(id: String): FrostWebCompose {
|
||||
val frostWebComponent = frostWebComponentBuilder.id(id).build()
|
||||
val frostWebEntryPoint = EntryPoints.get(frostWebComponent, FrostWebEntryPoint::class.java)
|
||||
return frostWebEntryPoint.frostWebCompose()
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(FrostWebComponent::class)
|
||||
interface FrostWebEntryPoint {
|
||||
fun frostWebCompose(): FrostWebCompose
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.webview
|
||||
|
||||
import com.pitchedapps.frost.compose.webview.FrostChromeClient
|
||||
import com.pitchedapps.frost.compose.webview.FrostWebCompose
|
||||
import com.pitchedapps.frost.compose.webview.FrostWebViewClient
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.FrostWebHelper
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import javax.inject.Inject
|
||||
|
||||
class FrostWebComposer
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val store: FrostWebStore,
|
||||
private val webHelper: FrostWebHelper,
|
||||
) {
|
||||
|
||||
fun create(tabId: WebTargetId): FrostWebCompose {
|
||||
val client = FrostWebViewClient(tabId, store, webHelper)
|
||||
val chromeClient = FrostChromeClient(tabId, store)
|
||||
return FrostWebCompose(tabId, store, client, chromeClient)
|
||||
}
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath('com.google.dagger:hilt-android-gradle-plugin:2.43.2')
|
||||
// https://mvnrepository.com/artifact/com.google.dagger/hilt-android-gradle-plugin
|
||||
classpath('com.google.dagger:hilt-android-gradle-plugin:2.46.1')
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version '8.0.2' apply false
|
||||
// https://mvnrepository.com/artifact/com.android.application/com.android.application.gradle.plugin?repo=google
|
||||
id 'com.android.application' version '8.1.0-beta04' apply false
|
||||
id 'com.android.library' version '8.0.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
|
||||
// https://mvnrepository.com/artifact/com.google.devtools.ksp/com.google.devtools.ksp.gradle.plugin
|
||||
|
Loading…
Reference in New Issue
Block a user