diff options
author | Allan Wang <me@allanwang.ca> | 2017-12-21 02:16:34 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-12-21 02:16:34 -0500 |
commit | d683cae6ffe644a9f63eea6cf3b7e59d2bde617b (patch) | |
tree | 517fe1d44c27084ccd87507d9804ba28f15c1647 /app/src/main/kotlin/com/pitchedapps/frost/fragments | |
parent | 82f9aca96493316bc62008f2b3167d34a6029b38 (diff) | |
download | frost-d683cae6ffe644a9f63eea6cf3b7e59d2bde617b.tar.gz frost-d683cae6ffe644a9f63eea6cf3b7e59d2bde617b.tar.bz2 frost-d683cae6ffe644a9f63eea6cf3b7e59d2bde617b.zip |
Enhancement/fragment interface (#564)
* Begin fragment interfaces and themable contracts
* Prepare swiperefresh interface
* Snapshot
* Add compilable version
* Revamp once more
* Finalize layouts
* Cleanup
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/fragments')
3 files changed, 326 insertions, 133 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt new file mode 100644 index 00000000..498164c0 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt @@ -0,0 +1,234 @@ +package com.pitchedapps.frost.fragments + +import android.content.Context +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import ca.allanwang.kau.utils.withArguments +import com.mikepenz.fastadapter.IItem +import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter +import com.pitchedapps.frost.R +import com.pitchedapps.frost.contracts.DynamicUiContract +import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.contracts.MainActivityContract +import com.pitchedapps.frost.enums.FeedSort +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.parsers.FrostParser +import com.pitchedapps.frost.utils.* +import com.pitchedapps.frost.views.FrostRecyclerView +import com.pitchedapps.frost.views.FrostWebView +import com.pitchedapps.frost.web.FrostWebViewClient +import com.pitchedapps.frost.web.FrostWebViewClientMenu +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.toast + +/** + * Created by Allan Wang on 2017-11-07. + */ +abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract { + + companion object { + private const val ARG_URL_ENUM = "arg_url_enum" + private const val ARG_POSITION = "arg_position" + + internal operator fun invoke(base: () -> BaseFragment, data: FbItem, position: Int): BaseFragment { + val fragment = if (Prefs.nativeViews) base() else WebFragment() + val d = if (data == FbItem.FEED) FeedSort(Prefs.feedSort).item else data + fragment.withArguments( + ARG_URL to d.url, + ARG_POSITION to position, + ARG_URL_ENUM to d + ) + return fragment + } + } + + override val baseUrl: String by lazy { arguments!!.getString(ARG_URL) } + override val baseEnum: FbItem by lazy { arguments!!.getSerializable(ARG_URL_ENUM) as FbItem } + override val position: Int by lazy { arguments!!.getInt(ARG_POSITION) } + + override var firstLoad: Boolean = true + private var activityDisposable: Disposable? = null + private var onCreateRunnable: ((FragmentContract) -> Unit)? = null + + override var content: FrostContentParent? = null + + protected abstract val layoutRes: Int + + override final fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(layoutRes, container, false) + val content = view as? FrostContentParent + ?: throw IllegalArgumentException("layoutRes for fragment must return view implementing FrostContentParent") + this.content = content + content.bind(this) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + onCreateRunnable?.invoke(this) + onCreateRunnable = null + firstLoadRequest() + } + + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + super.setUserVisibleHint(isVisibleToUser) + firstLoadRequest() + } + + override fun firstLoadRequest() { + if (userVisibleHint && isVisible && firstLoad) { + core?.reloadBase(true) + firstLoad = false + } + } + + override fun post(action: (fragment: FragmentContract) -> Unit) { + onCreateRunnable = action + } + + override fun setTitle(title: String) { + (context as? MainActivityContract)?.setTitle(title) + } + + override fun attachMainObservable(contract: MainActivityContract): Disposable = + contract.fragmentSubject.observeOn(AndroidSchedulers.mainThread()).subscribe { + when (it) { + REQUEST_REFRESH -> { + core?.apply { + reload(true) + clearHistory() + } + } + position -> { + contract.setTitle(baseEnum.titleId) + core?.active = true + } + -(position + 1) -> { + core?.active = false + } + REQUEST_TEXT_ZOOM -> { + reloadTextSize() + } + } + } + + override fun detachMainObservable() { + activityDisposable?.dispose() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + detachMainObservable() + if (context is MainActivityContract) + activityDisposable = attachMainObservable(context) + } + + override fun onDetach() { + detachMainObservable() + super.onDetach() + } + + override fun onDestroyView() { + content?.destroy() + content = null + super.onDestroyView() + } + + override fun reloadTheme() { + reloadThemeSelf() + content?.reloadTextSize() + } + + override fun reloadThemeSelf() { + // intentionally blank + } + + override fun reloadTextSize() { + reloadTextSizeSelf() + content?.reloadTextSize() + } + + override fun reloadTextSizeSelf() { + // intentionally blank + } + + override fun onBackPressed(): Boolean = content?.core?.onBackPressed() ?: false + + override fun onTabClick(): Unit = content?.core?.onTabClicked() ?: Unit +} + +abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), RecyclerContentContract { + + override val layoutRes: Int = R.layout.view_content_recycler + + /** + * The parser to make this all happen + */ + abstract val parser: FrostParser<T> + + abstract val adapter: FastItemAdapter<Item> + + abstract fun toItems(data: T): List<Item> + + override fun bind(recyclerView: FrostRecyclerView) { + recyclerView.adapter = this.adapter + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val tail = tailMapper(baseEnum) + if (tail.isNotEmpty()) { + val baseUrl = baseEnum.url + L.d("Adding $tail to $baseUrl for RecyclerFragment") + arguments!!.putString(ARG_URL, "$baseUrl$tail") + } + } + + private fun tailMapper(item: FbItem) = when (item) { + FbItem.NOTIFICATIONS, FbItem.MESSAGES -> "/?more" + else -> "" + } + + override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) { + doAsync { + progress(10) + val doc = frostJsoup(baseUrl) + progress(60) + val data = parser.parse(doc) + if (data == null) { + context?.toast(R.string.error_generic) + L.eThrow("RecyclerFragment failed for ${baseEnum.name}") + Prefs.nativeViews = false + return@doAsync callback(false) + } + progress(80) + val items = toItems(data) + progress(97) + adapter.setNewList(items) + } + } +} + +open class WebFragment : BaseFragment(), FragmentContract { + + override val layoutRes: Int = R.layout.view_content_web + + /** + * Given a webview, output a client + */ + open fun client(web: FrostWebView) = FrostWebViewClient(web) + + override fun innerView(context: Context) = FrostWebView(context) + +} + +class WebFragmentMenu : WebFragment() { + + override fun client(web: FrostWebView) = FrostWebViewClientMenu(web) + +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt new file mode 100644 index 00000000..62b1de33 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt @@ -0,0 +1,92 @@ +package com.pitchedapps.frost.fragments + +import android.content.Context +import com.pitchedapps.frost.contracts.FrostContentContainer +import com.pitchedapps.frost.contracts.FrostContentCore +import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.contracts.MainActivityContract +import com.pitchedapps.frost.views.FrostRecyclerView +import io.reactivex.disposables.Disposable + +/** + * Created by Allan Wang on 2017-11-07. + */ + +interface FragmentContract : FrostContentContainer { + + val content: FrostContentParent? + + /** + * Helper to retrieve the core from [content] + */ + val core: FrostContentCore? + get() = content?.core + + /** + * Specifies position in Activity's viewpager + */ + val position: Int + + /** + * Specifies whether if current load + * will be fragment's first load + * + * Defaults to true + */ + var firstLoad: Boolean + + /** + * Called when the fragment is first visible + * Typically, if [firstLoad] is true, + * the fragment should call [reload] and make [firstLoad] false + */ + fun firstLoadRequest() + + /** + * Single callable action to be executed upon creation + * Note that this call is not guaranteed + */ + fun post(action: (fragment: FragmentContract) -> Unit) + + /** + * Call whenever a fragment is attached so that it may listen + * to activity emissions + */ + fun attachMainObservable(contract: MainActivityContract): Disposable + + /** + * Load custom layout to container + */ + fun innerView(context: Context): FrostContentCore + + /** + * Call when fragment is detached so that any existing + * observable is disposed + */ + fun detachMainObservable() + + /* + * ----------------------------------------- + * Delegates + * ----------------------------------------- + */ + + fun onBackPressed(): Boolean + + fun onTabClick() + + +} + +interface RecyclerContentContract { + + fun bind(recyclerView: FrostRecyclerView) + + /** + * Completely handle data reloading + * Optional progress emission update + * Callback returns [true] for success, [false] otherwise + */ + fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) + +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt deleted file mode 100644 index f1b76e57..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.pitchedapps.frost.fragments - -import android.content.Context -import android.os.Bundle -import android.support.v4.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import ca.allanwang.kau.utils.withArguments -import com.pitchedapps.frost.activities.MainActivity -import com.pitchedapps.frost.enums.FeedSort -import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.utils.Prefs -import com.pitchedapps.frost.web.FrostWebView -import com.pitchedapps.frost.web.FrostWebViewCore -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable - -/** - * Created by Allan Wang on 2017-05-29. - */ - - -class WebFragment : Fragment() { - - companion object { - private const val ARG_URL = "arg_url" - private const val ARG_URL_ENUM = "arg_url_enum" - private const val ARG_POSITION = "arg_position" - const val REQUEST_TEXT_ZOOM = 17 - const val REQUEST_REFRESH = 99 - - operator fun invoke(data: FbItem, position: Int) = WebFragment().apply { - val d = if (data == FbItem.FEED) FeedSort(Prefs.feedSort).item else data - withArguments( - ARG_URL to d.url, - ARG_POSITION to position, - ARG_URL_ENUM to d - ) - } - } - - // val refresh: SwipeRefreshLayout by lazy { frostWebView.refresh } - val web: FrostWebViewCore by lazy { frostWebView.web } - val url: String by lazy { arguments!!.getString(ARG_URL) } - val urlEnum: FbItem by lazy { arguments!!.getSerializable(ARG_URL_ENUM) as FbItem } - val position: Int by lazy { arguments!!.getInt(ARG_POSITION) } - lateinit var frostWebView: FrostWebView - private var firstLoad = true - private var activityDisposable: Disposable? = null - private var onCreateRunnable: ((fragment: WebFragment) -> Unit)? = null - - /** - * Hook to run action once fragment is properly created - * This is not saved elsewhere and may not always execute - */ - fun post(action: (fragment: WebFragment) -> Unit) { - onCreateRunnable = action - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - super.onCreateView(inflater, container, savedInstanceState) - frostWebView = FrostWebView(context!!) - frostWebView.setupWebview(url, urlEnum) - return frostWebView - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - onCreateRunnable?.invoke(this) - onCreateRunnable = null - firstLoad() - } - - override fun setUserVisibleHint(isVisibleToUser: Boolean) { - super.setUserVisibleHint(isVisibleToUser) - firstLoad() - } - - fun firstLoad() { - if (userVisibleHint && isVisible && firstLoad) { - web.loadBaseUrl() - firstLoad = false - } - } - - override fun onAttach(context: Context) { - super.onAttach(context) - activityDisposable?.dispose() - if (context is MainActivity) { - activityDisposable = context.webFragmentObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { - /** - * Execute actions based on flags - * Flags between -10 and 10 are reserved for viewpager events - */ - when (it) { - REQUEST_REFRESH -> { - web.clearHistory() - web.loadBaseUrl(true) - } - position -> { - context.toolbar.setTitle(urlEnum.titleId) - pauseLoad = false - } - -(position + 1) -> { //we are moving away from this fragment - if (!frostWebView.refresh.isRefreshing) pauseLoad = true - } - REQUEST_TEXT_ZOOM -> frostWebView.web.settings.textZoom = Prefs.webTextScaling - } - } - } - } - - override fun onDetach() { - activityDisposable?.dispose() - super.onDetach() - } - - override fun onResume() { - super.onResume() - pauseLoad = false - firstLoad() - } - - var pauseLoad: Boolean - get() = web.settings.blockNetworkLoads - set(value) { - web.settings.blockNetworkLoads = value - } - - - fun onBackPressed() = frostWebView.onBackPressed() -}
\ No newline at end of file |