mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-11-09 20:42:34 +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"?>
|
<?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>
|
||||||
|
@ -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}"
|
||||||
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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
|
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 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"))
|
||||||
}
|
}
|
||||||
|
@ -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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,19 +35,26 @@ 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
|
||||||
|
|
||||||
|
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. */
|
/** Action to load new url. */
|
||||||
data class LoadUrlAction(val url: String) : UserAction
|
data class LoadUrlAction(val url: String) : UserAction
|
||||||
@ -54,12 +62,13 @@ sealed interface UserAction : FrostWebAction {
|
|||||||
object GoBackAction : UserAction
|
object GoBackAction : UserAction
|
||||||
|
|
||||||
object GoForwardAction : UserAction
|
object GoForwardAction : UserAction
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Response triggered by webview, indicating [UserAction] fulfillment. */
|
/** Response triggered by webview, indicating [UserAction] fulfillment. */
|
||||||
sealed interface ResponseAction : FrostWebAction {
|
sealed interface ResponseAction : Action {
|
||||||
|
|
||||||
data class LoadUrlResponseAction(val url: String) : ResponseAction
|
data class LoadUrlResponseAction(val url: String) : ResponseAction
|
||||||
|
|
||||||
data class WebStepResponseAction(val steps: Int) : ResponseAction
|
data class WebStepResponseAction(val steps: Int) : ResponseAction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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 }
|
||||||
|
}
|
||||||
|
@ -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
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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.
|
// 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
|
||||||
|
Loading…
Reference in New Issue
Block a user