diff options
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/views')
3 files changed, 96 insertions, 85 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt index c30ee199..16c28c02 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -22,7 +22,6 @@ import android.util.AttributeSet import android.view.View import android.widget.FrameLayout import android.widget.ProgressBar -import ca.allanwang.kau.utils.ContextHelper import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.circularReveal import ca.allanwang.kau.utils.fadeIn @@ -39,17 +38,27 @@ import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.WEB_LOAD_DELAY import com.pitchedapps.frost.injectors.ThemeProvider -import com.pitchedapps.frost.kotlin.subscribeDuringJob import com.pitchedapps.frost.prefs.Prefs import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.web.FrostEmitter +import com.pitchedapps.frost.web.asFrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.ConflatedBroadcastChannel -import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.flow.transformWhile import javax.inject.Inject +@ExperimentalCoroutinesApi class FrostContentWeb @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -60,6 +69,7 @@ class FrostContentWeb @JvmOverloads constructor( override val layoutRes: Int = R.layout.view_content_base_web } +@ExperimentalCoroutinesApi class FrostContentRecycler @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -70,7 +80,7 @@ class FrostContentRecycler @JvmOverloads constructor( override val layoutRes: Int = R.layout.view_content_base_recycler } -@UseExperimental(ExperimentalCoroutinesApi::class) +@ExperimentalCoroutinesApi abstract class FrostContentView<out T> @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -88,8 +98,8 @@ abstract class FrostContentView<out T> @JvmOverloads constructor( /** * Subsection of [FrostContentView] that is [AndroidEntryPoint] friendly (no generics) */ -@UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint +@ExperimentalCoroutinesApi abstract class FrostContentViewBase( context: Context, attrs: AttributeSet?, @@ -119,13 +129,35 @@ abstract class FrostContentViewBase( private val refresh: SwipeRefreshLayout by bindView(R.id.content_refresh) private val progress: ProgressBar by bindView(R.id.content_progress) + private val coreView: View by bindView(R.id.content_core) + /** * While this can be conflated, there exist situations where we wish to watch refresh cycles. * Here, we'd need to make sure we don't skip events + * + * TODO ensure there is only one flow provider is this is still separated in login + * Use case for shared flow is to avoid emitting before subscribing; buffer can probably be size 1 */ - override val refreshChannel: BroadcastChannel<Boolean> = BroadcastChannel(10) - override val progressChannel: BroadcastChannel<Int> = ConflatedBroadcastChannel() - override val titleChannel: BroadcastChannel<String> = ConflatedBroadcastChannel() + private val refreshMutableFlow = MutableSharedFlow<Boolean>( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + override val refreshFlow: SharedFlow<Boolean> = refreshMutableFlow.asSharedFlow() + + override val refreshEmit: FrostEmitter<Boolean> = refreshMutableFlow.asFrostEmitter() + + private val progressMutableFlow = MutableStateFlow(0) + + override val progressFlow: SharedFlow<Int> = progressMutableFlow.asSharedFlow() + + override val progressEmit: FrostEmitter<Int> = progressMutableFlow.asFrostEmitter() + + private val titleMutableFlow = MutableStateFlow("") + + override val titleFlow: SharedFlow<String> = titleMutableFlow.asSharedFlow() + + override val titleEmit: FrostEmitter<String> = titleMutableFlow.asFrostEmitter() override lateinit var scope: CoroutineScope @@ -160,7 +192,6 @@ abstract class FrostContentViewBase( */ protected fun init() { inflate(context, layoutRes, this) - core.parent = this reloadThemeSelf() } @@ -169,23 +200,23 @@ abstract class FrostContentViewBase( baseEnum = container.baseEnum init() scope = container - core.bind(container) + core.bind(this, container) refresh.setOnRefreshListener { core.reload(true) } - refreshChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { r -> + refreshFlow.distinctUntilChanged().onEach { r -> L.v { "Refreshing $r" } refresh.isRefreshing = r - } + }.launchIn(scope) - progressChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { p -> + progressFlow.onEach { p -> progress.invisibleIf(p == 100) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progress.setProgress(p, true) else progress.progress = p - } + }.launchIn(scope) } override fun reloadTheme() { @@ -212,7 +243,6 @@ abstract class FrostContentViewBase( } private var transitionStart: Long = -1 - private var refreshReceiver: ReceiveChannel<Boolean>? = null /** * Hook onto the refresh observable for one cycle @@ -220,33 +250,37 @@ abstract class FrostContentViewBase( * The cycle only starts on the first load since there may have been another process when this is registered */ override fun registerTransition(urlChanged: Boolean, animate: Boolean): Boolean { - if (!urlChanged && refreshReceiver != null) { + if (!urlChanged && transitionStart != -1L) { L.v { "Consuming url load" } return false // still in progress; do not bother with load } + coreView.transition(animate) + return true + } + + private fun View.transition(animate: Boolean) { L.v { "Registered transition" } - with(core) { - refreshReceiver = refreshChannel.openSubscription().also { receiver -> - scope.launchMain { - var loading = false - for (r in receiver) { - if (r) { - loading = true - transitionStart = System.currentTimeMillis() - clearAnimation() - if (isVisible) - fadeOut(duration = 200L) - } else if (loading) { - if (animate && prefs.animate) circularReveal(offset = WEB_LOAD_DELAY) - else fadeIn(duration = 200L, offset = WEB_LOAD_DELAY) - L.v { "Transition loaded in ${System.currentTimeMillis() - transitionStart} ms" } - receiver.cancel() - refreshReceiver = null - } + transitionStart = 0L // Marker for pending transition + scope.launchMain { + refreshFlow.distinctUntilChanged() + // Pseudo windowed mode + .runningFold(false to false) { (_, prev), curr -> prev to curr } + // Take until prev was loading and current is not loading + // Unlike takeWhile, we include the last state (first non matching) + .transformWhile { emit(it); it != (true to false) } + .onEach { (prev, curr) -> + if (curr) { + transitionStart = System.currentTimeMillis() + clearAnimation() + if (isVisible) + fadeOut(duration = 200L) + } else if (prev) { // prev && !curr + if (animate && prefs.animate) circularReveal(offset = WEB_LOAD_DELAY) + else fadeIn(duration = 200L, offset = WEB_LOAD_DELAY) + L.v { "Transition loaded in ${System.currentTimeMillis() - transitionStart} ms" } } - } - } + }.collect() + transitionStart = -1L } - return true } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt index 2ab00916..04ee7f3c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -29,7 +29,6 @@ import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.fragments.RecyclerContentContract import com.pitchedapps.frost.prefs.Prefs import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import javax.inject.Inject @@ -37,7 +36,6 @@ import javax.inject.Inject * Created by Allan Wang on 2017-05-29. * */ -@UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint class FrostRecyclerView @JvmOverloads constructor( context: Context, @@ -61,7 +59,8 @@ class FrostRecyclerView @JvmOverloads constructor( layoutManager = LinearLayoutManager(context) } - override fun bind(container: FrostContentContainer): View { + override fun bind(parent: FrostContentParent, container: FrostContentContainer): View { + this.parent = parent if (container !is RecyclerContentContract) throw IllegalStateException("FrostRecyclerView must bind to a container that is a RecyclerContentContract") this.recyclerContract = container @@ -78,10 +77,10 @@ class FrostRecyclerView @JvmOverloads constructor( override fun reloadBase(animate: Boolean) { if (prefs.animate) fadeOut(onFinish = onReloadClear) scope.launch { - parent.refreshChannel.offer(true) - recyclerContract.reload { parent.progressChannel.offer(it) } - parent.progressChannel.offer(100) - parent.refreshChannel.offer(false) + parent.refreshEmit(true) + recyclerContract.reload { parent.progressEmit(it) } + parent.progressEmit(100) + parent.refreshEmit(false) if (prefs.animate) circularReveal() } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt index f384d134..140b4901 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt @@ -35,25 +35,23 @@ import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.currentCookie import com.pitchedapps.frost.facebook.FB_HOME_URL import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.USER_AGENT -import com.pitchedapps.frost.fragments.WebFragment import com.pitchedapps.frost.injectors.ThemeProvider import com.pitchedapps.frost.prefs.Prefs import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostDownload import com.pitchedapps.frost.web.FrostChromeClient -import com.pitchedapps.frost.web.FrostJSI +import com.pitchedapps.frost.web.FrostWebClientEntryPoint +import com.pitchedapps.frost.web.FrostWebComponentBuilder +import com.pitchedapps.frost.web.FrostWebEntryPoint import com.pitchedapps.frost.web.FrostWebViewClient +import com.pitchedapps.frost.web.FrostWebViewClientMenu +import com.pitchedapps.frost.web.FrostWebViewClientMessenger import com.pitchedapps.frost.web.NestedWebView -import dagger.BindsInstance -import dagger.hilt.DefineComponent -import dagger.hilt.EntryPoint import dagger.hilt.EntryPoints -import dagger.hilt.InstallIn import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.components.ViewComponent import javax.inject.Inject -import javax.inject.Scope import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -103,9 +101,11 @@ class FrostWebView @JvmOverloads constructor( get() = url ?: "" @SuppressLint("SetJavaScriptEnabled") - override fun bind(container: FrostContentContainer): View { - val component = frostWebComponentBuilder.frostWebView(this).build() - val entryPoint = EntryPoints.get(component, FrostWebEntryPoint::class.java) + override fun bind(parent: FrostContentParent, container: FrostContentContainer): View { + this.parent = parent + val component = frostWebComponentBuilder.frostParent(parent).frostWebView(this).build() + val webEntryPoint = EntryPoints.get(component, FrostWebEntryPoint::class.java) + val clientEntryPoint = EntryPoints.get(component, FrostWebClientEntryPoint::class.java) userAgentString = USER_AGENT with(settings) { javaScriptEnabled = true @@ -116,10 +116,14 @@ class FrostWebView @JvmOverloads constructor( } setLayerType(LAYER_TYPE_HARDWARE, null) // attempt to get custom client; otherwise fallback to original - frostWebClient = (container as? WebFragment)?.client(this) ?: FrostWebViewClient(this) + frostWebClient = when (parent.baseEnum) { + FbItem.MESSENGER -> FrostWebViewClientMessenger(this) + FbItem.MENU -> FrostWebViewClientMenu(this) + else -> clientEntryPoint.webClient() + } webViewClient = frostWebClient webChromeClient = FrostChromeClient(this, themeProvider, webFileChooser) - addJavascriptInterface(entryPoint.frostJsi(), "Frost") + addJavascriptInterface(webEntryPoint.frostJsi(), "Frost") setBackgroundColor(Color.TRANSPARENT) setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> context.ctxCoroutine.launchMain { @@ -251,29 +255,3 @@ class FrostWebView @JvmOverloads constructor( super.destroy() } } - -@Scope -@Retention(AnnotationRetention.BINARY) -@Target( - AnnotationTarget.FUNCTION, - AnnotationTarget.TYPE, - AnnotationTarget.CLASS -) -annotation class FrostWebScoped - -@FrostWebScoped -@DefineComponent(parent = ViewComponent::class) -interface FrostWebComponent - -@DefineComponent.Builder -interface FrostWebComponentBuilder { - fun frostWebView(@BindsInstance web: FrostWebView): FrostWebComponentBuilder - fun build(): FrostWebComponent -} - -@EntryPoint -@InstallIn(FrostWebComponent::class) -interface FrostWebEntryPoint { - @FrostWebScoped - fun frostJsi(): FrostJSI -} |