aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/fragments
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-12-21 02:16:34 -0500
committerGitHub <noreply@github.com>2017-12-21 02:16:34 -0500
commitd683cae6ffe644a9f63eea6cf3b7e59d2bde617b (patch)
tree517fe1d44c27084ccd87507d9804ba28f15c1647 /app/src/main/kotlin/com/pitchedapps/frost/fragments
parent82f9aca96493316bc62008f2b3167d34a6029b38 (diff)
downloadfrost-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')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt234
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt92
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt133
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