/* * Copyright 2018 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 . */ package com.pitchedapps.frost.views import android.content.Context import android.os.Build 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 import ca.allanwang.kau.utils.fadeOut import ca.allanwang.kau.utils.invisibleIf import ca.allanwang.kau.utils.isVisible import ca.allanwang.kau.utils.launchMain import ca.allanwang.kau.utils.tint import ca.allanwang.kau.utils.withAlpha import com.pitchedapps.frost.R import com.pitchedapps.frost.contracts.FrostContentContainer import com.pitchedapps.frost.contracts.FrostContentCore 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 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 javax.inject.Inject class FrostContentWeb @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : FrostContentView(context, attrs, defStyleAttr, defStyleRes) { override val layoutRes: Int = R.layout.view_content_base_web } class FrostContentRecycler @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : FrostContentView(context, attrs, defStyleAttr, defStyleRes) { override val layoutRes: Int = R.layout.view_content_base_recycler } @UseExperimental(ExperimentalCoroutinesApi::class) abstract class FrostContentView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : FrostContentViewBase(context, attrs, defStyleAttr, defStyleRes), FrostContentParent where T : View, T : FrostContentCore { val coreView: T by bindView(R.id.content_core) override val core: FrostContentCore get() = coreView } /** * Subsection of [FrostContentView] that is [AndroidEntryPoint] friendly (no generics) */ @UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint abstract class FrostContentViewBase( context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int ) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), FrostContentParent { // No JvmOverloads due to hilt constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this( context, attrs, defStyleAttr, 0 ) @Inject lateinit var prefs: Prefs @Inject lateinit var themeProvider: ThemeProvider private val refresh: SwipeRefreshLayout by bindView(R.id.content_refresh) private val progress: ProgressBar by bindView(R.id.content_progress) /** * 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 */ override val refreshChannel: BroadcastChannel = BroadcastChannel(10) override val progressChannel: BroadcastChannel = ConflatedBroadcastChannel() override val titleChannel: BroadcastChannel = ConflatedBroadcastChannel() override lateinit var scope: CoroutineScope override lateinit var baseUrl: String override var baseEnum: FbItem? = null protected abstract val layoutRes: Int @Volatile override var swipeDisabledByAction = false set(value) { field = value updateSwipeEnabler() } @Volatile override var swipeAllowedByPage: Boolean = true set(value) { field = value updateSwipeEnabler() } private fun updateSwipeEnabler() { val swipeEnabled = swipeAllowedByPage && !swipeDisabledByAction if (refresh.isEnabled == swipeEnabled) return refresh.post { refresh.isEnabled = swipeEnabled } } /** * Sets up everything * Called by [bind] */ protected fun init() { inflate(context, layoutRes, this) core.parent = this reloadThemeSelf() } override fun bind(container: FrostContentContainer) { baseUrl = container.baseUrl baseEnum = container.baseEnum init() scope = container core.bind(container) refresh.setOnRefreshListener { core.reload(true) } refreshChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { r -> L.v { "Refreshing $r" } refresh.isRefreshing = r } progressChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { p -> progress.invisibleIf(p == 100) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progress.setProgress(p, true) else progress.progress = p } } override fun reloadTheme() { reloadThemeSelf() core.reloadTheme() } override fun reloadTextSize() { core.reloadTextSize() } override fun reloadThemeSelf() { progress.tint(themeProvider.textColor.withAlpha(180)) refresh.setColorSchemeColors(themeProvider.iconColor) refresh.setProgressBackgroundColorSchemeColor(themeProvider.headerColor.withAlpha(255)) } override fun reloadTextSizeSelf() { // intentionally blank } override fun destroy() { core.destroy() } private var transitionStart: Long = -1 private var refreshReceiver: ReceiveChannel? = null /** * Hook onto the refresh observable for one cycle * Animate toggles between the fancy ripple and the basic fade * 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) { L.v { "Consuming url load" } return false // still in progress; do not bother with load } 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 } } } } } return true } }