/* * 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 androidx.swiperefreshlayout.widget.SwipeRefreshLayout 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.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.utils.L import com.pitchedapps.frost.utils.Prefs import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import io.reactivex.rxkotlin.addTo import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext 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 ) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), FrostContentParent where T : View, T : FrostContentCore { private val refresh: SwipeRefreshLayout by bindView(R.id.content_refresh) private val progress: ProgressBar by bindView(R.id.content_progress) val coreView: T by bindView(R.id.content_core) override val core: FrostContentCore get() = coreView override val progressObservable: PublishSubject = PublishSubject.create() override val refreshObservable: PublishSubject = PublishSubject.create() override val titleObservable: BehaviorSubject = BehaviorSubject.create() override val refreshChannel: BroadcastChannel = BroadcastChannel(Channel.UNLIMITED) override val progressChannel: BroadcastChannel = BroadcastChannel(Channel.UNLIMITED) override val titleChannel: BroadcastChannel = BroadcastChannel(Channel.UNLIMITED) override lateinit var scope: CoroutineScope override lateinit var baseUrl: String override var baseEnum: FbItem? = null protected abstract val layoutRes: Int override var swipeEnabled = true set(value) { if (field == value) return field = value refresh.post { refresh.isEnabled = value } } /** * Sets up everything * Called by [bind] */ protected fun init() { inflate(context, layoutRes, this) coreView.parent = this reloadThemeSelf() } override fun bind(container: FrostContentContainer) { baseUrl = container.baseUrl baseEnum = container.baseEnum init() scope = container core.bind(container) refresh.setOnRefreshListener { with(coreView) { reload(true) } } scope.launch(Dispatchers.Default) { launch { for (r in refreshChannel.openSubscription()) { withContext(Dispatchers.Main) { refresh.isRefreshing = r refresh.isEnabled = true } } } launch { for (p in progressChannel.openSubscription()) { withContext(Dispatchers.Main) { 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() coreView.reloadTheme() } override fun reloadTextSize() { coreView.reloadTextSize() } override fun reloadThemeSelf() { progress.tint(Prefs.textColor.withAlpha(180)) refresh.setColorSchemeColors(Prefs.iconColor) refresh.setProgressBackgroundColorSchemeColor(Prefs.headerColor.withAlpha(255)) } override fun reloadTextSizeSelf() { // intentionally blank } override fun destroy() { titleChannel.close() progressChannel.close() refreshChannel.close() core.destroy() } private var dispose: Disposable? = null private var transitionStart: Long = -1 /** * 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 && dispose != null) { L.v { "Consuming url load" } return false // still in progress; do not bother with load } L.v { "Registered transition" } with(coreView) { var loading = dispose != null dispose?.dispose() dispose = refreshObservable .observeOn(AndroidSchedulers.mainThread()) .subscribe { if (it) { loading = true transitionStart = System.currentTimeMillis() clearAnimation() if (isVisible) fadeOut(duration = 200L) } else if (loading) { loading = false 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" } dispose?.dispose() dispose = null } } } return true } }