1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-09-19 15:11:42 +02:00

Convert FrostWebStore into singleton

This commit is contained in:
Allan Wang 2023-06-20 01:06:09 -07:00
parent 2794a104e8
commit 3bd266feb4
No known key found for this signature in database
GPG Key ID: C93E3F9C679D7A56
21 changed files with 765 additions and 277 deletions

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="17" />
</component>
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.21" /> <option name="version" value="1.8.21" />
</component> </component>

View File

@ -156,7 +156,7 @@ dependencies {
androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1" 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}" implementation "com.google.dagger:hilt-android:${hilt}"
kapt "com.google.dagger:hilt-android-compiler:${hilt}" kapt "com.google.dagger:hilt-android-compiler:${hilt}"

View File

@ -21,15 +21,25 @@ import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import com.google.common.flogger.FluentLogger 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.FrostWebStore
import com.pitchedapps.frost.web.state.UpdateProgressAction import com.pitchedapps.frost.web.state.TabAction
import com.pitchedapps.frost.web.state.UpdateTitleAction 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 import javax.inject.Inject
/** The default chrome client */ /** 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() { WebChromeClient() {
private fun FrostWebStore.dispatch(action: TabAction.Action) {
dispatch(TabAction(tabId = tabId, action = action))
}
override fun getDefaultVideoPoster(): Bitmap? = override fun getDefaultVideoPoster(): Bitmap? =
super.getDefaultVideoPoster() ?: Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888) 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) { override fun onProgressChanged(view: WebView, newProgress: Int) {
super.onProgressChanged(view, newProgress) super.onProgressChanged(view, newProgress)
if (store.state.progress == 100) return // TODO remove?
if (store.state[tabId]?.progress == 100) return
store.dispatch(UpdateProgressAction(newProgress)) store.dispatch(UpdateProgressAction(newProgress))
} }

View File

@ -30,11 +30,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.children 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.FrostWebStore
import com.pitchedapps.frost.web.state.ResponseAction import com.pitchedapps.frost.web.state.TabAction
import com.pitchedapps.frost.webview.FrostWebScoped import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction
import javax.inject.Inject 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.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter 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.flow
import mozilla.components.lib.state.ext.observeAsState import mozilla.components.lib.state.ext.observeAsState
@FrostWebScoped class FrostWebCompose(
class FrostWebCompose val tabId: WebTargetId,
@Inject
internal constructor(
private val store: FrostWebStore, private val store: FrostWebStore,
private val client: FrostWebViewClient, private val client: FrostWebViewClient,
private val chromeClient: FrostChromeClient, private val chromeClient: FrostChromeClient,
) { ) {
private fun FrostWebStore.dispatch(action: TabAction.Action) {
dispatch(TabAction(tabId = tabId, action = action))
}
/** /**
* Webview implementation in compose * Webview implementation in compose
* *
@ -96,20 +101,20 @@ internal constructor(
webView?.let { wv -> webView?.let { wv ->
val lifecycleOwner = LocalLifecycleOwner.current 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() } BackHandler(captureBackPresses && canGoBack) { wv.goBack() }
LaunchedEffect(wv, store) { LaunchedEffect(wv, store) {
fun storeFlow(action: suspend Flow<FrostWebState>.() -> Unit) = launch { fun storeFlow(action: suspend Flow<TabWebState>.() -> Unit) = launch {
store.flow(lifecycleOwner).action() store.flow(lifecycleOwner).mapNotNull { it[tabId] }.action()
} }
storeFlow { storeFlow {
mapNotNull { it.transientState.targetUrl } mapNotNull { it.transientState.targetUrl }
.distinctUntilChanged() .distinctUntilChanged()
.collect { url -> .collect { url ->
store.dispatch(ResponseAction.LoadUrlResponseAction(url)) store.dispatch(LoadUrlResponseAction(url))
wv.loadUrl(url) wv.loadUrl(url)
} }
} }
@ -118,9 +123,11 @@ internal constructor(
.distinctUntilChanged() .distinctUntilChanged()
.filter { it != 0 } .filter { it != 0 }
.collect { steps -> .collect { steps ->
store.dispatch(ResponseAction.WebStepResponseAction(steps)) store.dispatch(WebStepResponseAction(steps))
if (wv.canGoBackOrForward(steps)) { if (wv.canGoBackOrForward(steps)) {
wv.goBackOrForward(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()
}
} }

View File

@ -22,14 +22,17 @@ import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import com.google.common.flogger.FluentLogger 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.FACEBOOK_BASE_COM
import com.pitchedapps.frost.facebook.WWW_FACEBOOK_COM import com.pitchedapps.frost.facebook.WWW_FACEBOOK_COM
import com.pitchedapps.frost.facebook.isExplicitIntent import com.pitchedapps.frost.facebook.isExplicitIntent
import com.pitchedapps.frost.web.FrostWebHelper import com.pitchedapps.frost.web.FrostWebHelper
import com.pitchedapps.frost.web.state.FrostWebStore import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.web.state.UpdateNavigationAction import com.pitchedapps.frost.web.state.TabAction
import com.pitchedapps.frost.web.state.UpdateProgressAction import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateNavigationAction
import com.pitchedapps.frost.web.state.UpdateTitleAction 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 java.io.ByteArrayInputStream
import javax.inject.Inject import javax.inject.Inject
@ -61,8 +64,15 @@ abstract class BaseWebViewClient : WebViewClient() {
/** The default webview client */ /** The default webview client */
class FrostWebViewClient class FrostWebViewClient
@Inject @Inject
internal constructor(private val store: FrostWebStore, override val webHelper: FrostWebHelper) : internal constructor(
BaseWebViewClient() { @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 */ /** True if current url supports refresh. See [doUpdateVisitedHistory] for updates */
internal var urlSupportsRefresh: Boolean = true internal var urlSupportsRefresh: Boolean = true

View File

@ -28,6 +28,8 @@ import kotlinx.coroutines.flow.map
*/ */
@JvmInline value class FrostAccountId(val id: Long) @JvmInline value class FrostAccountId(val id: Long)
@JvmInline value class WebTargetId(val id: String)
/** /**
* Representation of gecko context id. * Representation of gecko context id.
* *

View File

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

View File

@ -16,47 +16,15 @@
*/ */
package com.pitchedapps.frost.hilt package com.pitchedapps.frost.hilt
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import com.google.common.flogger.FluentLogger 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 com.pitchedapps.frost.web.FrostAdBlock
import dagger.BindsOptionalOf import dagger.BindsOptionalOf
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import java.util.Optional
import javax.inject.Qualifier import javax.inject.Qualifier
import javax.inject.Singleton 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 @Qualifier annotation class Frost
@ -68,7 +36,7 @@ interface FrostBindModule {
@BindsOptionalOf fun adBlock(): FrostAdBlock @BindsOptionalOf fun adBlock(): FrostAdBlock
} }
/** Module containing core Mozilla injections. */ /** Module containing core Frost injections. */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object FrostModule { 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" "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 @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))
}
} }

View File

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

View File

@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pitchedapps.frost.compose.webview.FrostWebCompose import com.pitchedapps.frost.compose.webview.FrostWebCompose
import com.pitchedapps.frost.ext.GeckoContextId import com.pitchedapps.frost.ext.GeckoContextId
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.ext.idData import com.pitchedapps.frost.ext.idData
import com.pitchedapps.frost.ext.toContextId import com.pitchedapps.frost.ext.toContextId
import com.pitchedapps.frost.extension.FrostCoreExtension import com.pitchedapps.frost.extension.FrostCoreExtension
@ -49,5 +50,5 @@ internal constructor(
var tabIndex: Int by mutableStateOf(0) var tabIndex: Int by mutableStateOf(0)
val frostWebCompose: FrostWebCompose = frostWebComposer.create("test") val frostWebCompose: FrostWebCompose = frostWebComposer.create(WebTargetId("test"))
} }

View File

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

View File

@ -20,13 +20,15 @@ import com.google.common.flogger.FluentLogger
import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext 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( override fun invoke(
context: MiddlewareContext<FrostWebState, FrostWebAction>, context: MiddlewareContext<FrostWebState, FrostWebAction>,
next: (FrostWebAction) -> Unit, next: (FrostWebAction) -> Unit,
action: FrostWebAction 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) next(action)
} }
@ -34,3 +36,15 @@ class FrostLoggerMiddleware(private val tag: String) : Middleware<FrostWebState,
private val logger = FluentLogger.forEnclosingClass() 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)
}
}
}

View File

@ -16,6 +16,7 @@
*/ */
package com.pitchedapps.frost.web.state package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.ext.WebTargetId
import mozilla.components.lib.state.Action import mozilla.components.lib.state.Action
/** /**
@ -34,32 +35,40 @@ sealed interface FrostWebAction : Action
*/ */
object InitAction : FrostWebAction object InitAction : FrostWebAction
/** Action indicating current url state. */ /** Action affecting a single tab */
data class UpdateUrlAction(val url: String) : FrostWebAction data class TabAction(val tabId: WebTargetId, val action: Action) : FrostWebAction {
sealed interface Action
/** Action indicating current title state. */ sealed interface ContentAction : Action {
data class UpdateTitleAction(val title: String?) : FrostWebAction
data class UpdateNavigationAction(val canGoBack: Boolean, val canGoForward: Boolean) : /** Action indicating current url state. */
FrostWebAction 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. */ data class UpdateNavigationAction(val canGoBack: Boolean, val canGoForward: Boolean) :
sealed interface UserAction : FrostWebAction { ContentAction
/** Action to load new url. */ data class UpdateProgressAction(val progress: Int) : ContentAction
data class LoadUrlAction(val url: String) : UserAction }
object GoBackAction : UserAction /** Action triggered by user, leading to transient state changes. */
sealed interface UserAction : Action {
object GoForwardAction : UserAction /** Action to load new url. */
} data class LoadUrlAction(val url: String) : UserAction
/** Response triggered by webview, indicating [UserAction] fulfillment. */ object GoBackAction : UserAction
sealed interface ResponseAction : FrostWebAction {
object GoForwardAction : UserAction
data class LoadUrlResponseAction(val url: String) : ResponseAction }
data class WebStepResponseAction(val steps: Int) : ResponseAction /** 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
}
} }

View File

@ -16,6 +16,9 @@
*/ */
package com.pitchedapps.frost.web.state package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.web.state.reducer.ContentStateReducer
/** /**
* See * 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 * 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 { fun reduce(state: FrostWebState, action: FrostWebAction): FrostWebState {
return when (action) { return when (action) {
is InitAction -> state is InitAction -> state
is UpdateUrlAction -> state.copy(url = action.url) is TabAction ->
is UpdateProgressAction -> state.copy(progress = action.progress) state.updateTabState(action.tabId) { ContentStateReducer.reduce(it, action.action) }
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,
),
)
} }
} }
} }
private object FrostTransientWebReducer { internal fun FrostWebState.updateTabState(
fun reduce(state: TransientWebState, action: UserAction): TransientWebState { tabId: WebTargetId,
return when (action) { update: (TabWebState) -> TabWebState,
is UserAction.LoadUrlAction -> state.copy(targetUrl = action.url) ): FrostWebState {
is UserAction.GoBackAction -> state.copy(navStep = state.navStep - 1) val floatingTabMatch = floatingTab?.takeIf { it.id == tabId }
is UserAction.GoForwardAction -> state.copy(navStep = state.navStep + 1) 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 { * Finds the corresponding tab in the list and replaces it using [update].
return when (action) { *
is ResponseAction.LoadUrlResponseAction -> * @param tabId ID of the tab to change.
if (state.targetUrl == action.url) state.copy(targetUrl = null) else state * @param update Returns a new version of the tab state.
is ResponseAction.WebStepResponseAction -> state.copy(navStep = state.navStep - action.steps) */
} 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)
} }

View File

@ -16,6 +16,8 @@
*/ */
package com.pitchedapps.frost.web.state package com.pitchedapps.frost.web.state
import com.pitchedapps.frost.ext.FrostAccountId
import com.pitchedapps.frost.ext.WebTargetId
import mozilla.components.lib.state.State import mozilla.components.lib.state.State
/** /**
@ -25,6 +27,39 @@ import mozilla.components.lib.state.State
* for Firefox example. * for Firefox example.
*/ */
data class FrostWebState( 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 baseUrl: String? = null,
val url: String? = null, val url: String? = null,
val title: String? = null, val title: String? = null,
@ -32,7 +67,7 @@ data class FrostWebState(
val canGoBack: Boolean = false, val canGoBack: Boolean = false,
val canGoForward: Boolean = false, val canGoForward: Boolean = false,
val transientState: TransientWebState = TransientWebState(), val transientState: TransientWebState = TransientWebState(),
) : State )
/** /**
* Transient web state. * Transient web state.

View File

@ -16,7 +16,7 @@
*/ */
package com.pitchedapps.frost.web.state 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.Middleware
import mozilla.components.lib.state.Store import mozilla.components.lib.state.Store
@ -27,7 +27,6 @@ import mozilla.components.lib.state.Store
* For firefox example. * For firefox example.
*/ */
class FrostWebStore( class FrostWebStore(
tag: String,
initialState: FrostWebState = FrostWebState(), initialState: FrostWebState = FrostWebState(),
middleware: List<Middleware<FrostWebState, FrostWebAction>> = emptyList(), middleware: List<Middleware<FrostWebState, FrostWebAction>> = emptyList(),
) : ) :
@ -35,10 +34,14 @@ class FrostWebStore(
initialState, initialState,
FrostWebReducer::reduce, FrostWebReducer::reduce,
middleware, middleware,
"FrostStore-$tag", "FrostStore",
) { ) {
init { init {
dispatch(InitAction) 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 }
}

View File

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

View File

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

View File

@ -16,18 +16,10 @@
*/ */
package com.pitchedapps.frost.webview package com.pitchedapps.frost.webview
import com.pitchedapps.frost.compose.webview.FrostWebCompose import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.web.state.FrostLoggerMiddleware
import com.pitchedapps.frost.web.state.FrostWebStore
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Module
import dagger.Provides
import dagger.hilt.DefineComponent import dagger.hilt.DefineComponent
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.components.ViewModelComponent
import javax.inject.Inject
import javax.inject.Qualifier import javax.inject.Qualifier
import javax.inject.Scope import javax.inject.Scope
@ -45,38 +37,11 @@ annotation class FrostWebScoped
@FrostWebScoped @DefineComponent(parent = ViewModelComponent::class) interface FrostWebComponent @FrostWebScoped @DefineComponent(parent = ViewModelComponent::class) interface FrostWebComponent
/** Using this component seems to be buggy, leading to an invalid param tabId error. */
@DefineComponent.Builder @DefineComponent.Builder
interface FrostWebComponentBuilder { interface FrostWebComponentBuilder {
fun id(@BindsInstance @FrostWeb id: String): FrostWebComponentBuilder
@BindsInstance fun tabId(@FrostWeb tabId: WebTargetId): FrostWebComponentBuilder
fun build(): FrostWebComponent 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
}
}

View File

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

View File

@ -1,12 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
dependencies { 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 { 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 'com.android.library' version '8.0.2' apply false
id 'org.jetbrains.kotlin.android' version '1.8.21' 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 // https://mvnrepository.com/artifact/com.google.devtools.ksp/com.google.devtools.ksp.gradle.plugin