aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt64
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt16
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Menu.kt158
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt119
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt149
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt41
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt17
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/iitems/GenericIItems.kt97
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt67
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt19
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt2
17 files changed, 615 insertions, 159 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
index 0d4ea46c..64a3d14c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
@@ -16,7 +16,6 @@ import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.TabLayout
import android.support.v4.app.Fragment
-import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentPagerAdapter
import android.support.v7.widget.Toolbar
import android.view.Menu
@@ -58,6 +57,7 @@ import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
import com.pitchedapps.frost.fragments.BaseFragment
+import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.parsers.FrostSearch
import com.pitchedapps.frost.parsers.SearchParser
import com.pitchedapps.frost.utils.*
@@ -80,7 +80,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
VideoViewHolder, SearchViewHolder,
FrostBilling by IabMain() {
- lateinit var adapter: SectionsPagerAdapter
+ protected lateinit var adapter: SectionsPagerAdapter
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
val toolbar: Toolbar by bindView(R.id.toolbar)
val viewPager: FrostViewPager by bindView(R.id.container)
@@ -114,7 +114,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
controlWebview = WebView(this)
setFrameContentView(Prefs.mainActivityLayout.layoutRes)
setSupportActionBar(toolbar)
- adapter = SectionsPagerAdapter(supportFragmentManager, loadFbTabs())
+ adapter = SectionsPagerAdapter(loadFbTabs())
viewPager.adapter = adapter
viewPager.offscreenPageLimit = TAB_COUNT
setupDrawer(savedInstanceState)
@@ -335,6 +335,19 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
}
}
+ private val STATE_FORCE_FALLBACK = "frost_state_force_fallback"
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(adapter.forcedFallbacks))
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ adapter.forcedFallbacks.clear()
+ adapter.forcedFallbacks.addAll(savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK))
+ }
+
override fun onResume() {
super.onResume()
FbCookie.switchBackUser { }
@@ -384,32 +397,41 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
inline val currentFragment
get() = supportFragmentManager.findFragmentByTag("android:switcher:${R.id.container}:${viewPager.currentItem}") as BaseFragment
- inner class SectionsPagerAdapter(fm: FragmentManager, val pages: List<FbItem>) : FragmentPagerAdapter(fm) {
+ override fun reloadFragment(fragment: BaseFragment) {
+ runOnUiThread { adapter.reloadFragment(fragment) }
+ }
+
+ inner class SectionsPagerAdapter(val pages: List<FbItem>) : FragmentPagerAdapter(supportFragmentManager) {
+
+ val forcedFallbacks = mutableSetOf<String>()
+
+ fun reloadFragment(fragment: BaseFragment) {
+ if (fragment is WebFragment) return
+ L.d("Reload fragment ${fragment.position}: ${fragment.baseEnum.name}")
+ forcedFallbacks.add(fragment.baseEnum.name)
+ supportFragmentManager.beginTransaction().remove(fragment).commitNowAllowingStateLoss()
+ notifyDataSetChanged()
+ }
override fun getItem(position: Int): Fragment {
val item = pages[position]
- val fragment = BaseFragment(item.fragmentCreator, item, position)
- //If first load hasn't occurred, add a listener
- // todo check
-// if (!firstLoadFinished) {
-// var disposable: Disposable? = null
-// fragment.post {
-// disposable = it.web.refreshObservable.subscribe {
-// if (!it) {
-// //Ensure first load finisher only happens once
-// if (!firstLoadFinished) firstLoadFinished = true
-// disposable?.dispose()
-// disposable = null
-// }
-// }
-// }
-// }
- return fragment
+ return BaseFragment(item.fragmentCreator,
+ forcedFallbacks.contains(item.name),
+ item,
+ position)
}
override fun getCount() = pages.size
override fun getPageTitle(position: Int): CharSequence = getString(pages[position].titleId)
+
+ override fun getItemPosition(fragment: Any) =
+ if (fragment !is BaseFragment)
+ POSITION_UNCHANGED
+ else if (fragment is WebFragment || fragment.valid)
+ POSITION_UNCHANGED
+ else
+ POSITION_NONE
}
override val lowerVideoPadding: PointF
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
index f72807d1..9b46a0a3 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
@@ -3,12 +3,18 @@ package com.pitchedapps.frost.activities
import android.os.Bundle
import android.support.design.widget.TabLayout
import android.support.v4.view.ViewPager
+import ca.allanwang.kau.utils.materialDialog
+import ca.allanwang.kau.utils.toast
+import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
-import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.facebook.requests.fbRequest
+import com.pitchedapps.frost.facebook.requests.getMenuData
import com.pitchedapps.frost.views.BadgedIcon
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.uiThread
import org.jsoup.Jsoup
import java.util.concurrent.TimeUnit
@@ -16,13 +22,7 @@ class MainActivity : BaseMainActivity() {
override val fragmentSubject = PublishSubject.create<Int>()!!
var lastPosition = -1
- val headerBadgeObservable = PublishSubject.create<String>()
- var firstLoadFinished = false
- set(value) {
- if (field && value) return //both vals are already true
- L.i("First fragment load has finished")
- field = value
- }
+ val headerBadgeObservable = PublishSubject.create<String>()!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
index e46a4bfb..559c2d0f 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
@@ -1,5 +1,6 @@
package com.pitchedapps.frost.contracts
+import com.pitchedapps.frost.fragments.BaseFragment
import io.reactivex.subjects.PublishSubject
/**
@@ -12,4 +13,5 @@ interface MainActivityContract : ActivityContract {
fun setTitle(res: Int)
fun setTitle(text: CharSequence)
fun collapseAppBar()
+ fun reloadFragment(fragment: BaseFragment)
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
index ad180023..9220c0e0 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
@@ -6,10 +6,7 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic
import com.pitchedapps.frost.R
-import com.pitchedapps.frost.fragments.BaseFragment
-import com.pitchedapps.frost.fragments.NotificationFragment
-import com.pitchedapps.frost.fragments.WebFragment
-import com.pitchedapps.frost.fragments.WebFragmentMenu
+import com.pitchedapps.frost.fragments.*
import com.pitchedapps.frost.utils.EnumBundle
import com.pitchedapps.frost.utils.EnumBundleCompanion
import com.pitchedapps.frost.utils.EnumCompanion
@@ -30,7 +27,7 @@ enum class FbItem(
FEED_TOP_STORIES(R.string.top_stories, GoogleMaterial.Icon.gmd_star, "home.php?sk=h_nor"),
FRIENDS(R.string.friends, GoogleMaterial.Icon.gmd_person_add, "friends/center/requests"),
GROUPS(R.string.groups, GoogleMaterial.Icon.gmd_group, "groups"),
- MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "settings", ::WebFragmentMenu),
+ MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "settings", ::MenuFragment),
MESSAGES(R.string.messages, MaterialDesignIconic.Icon.gmi_comments, "messages"),
NOTES(R.string.notes, CommunityMaterial.Icon.cmd_note, "notes"),
NOTIFICATIONS(R.string.notifications, MaterialDesignIconic.Icon.gmi_globe, "notifications", ::NotificationFragment),
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
index e3e77c5c..cefece36 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
@@ -19,7 +19,8 @@ private val authMap: MutableMap<String, RequestAuth> = mutableMapOf()
* [action] will only be called if a valid auth is found.
* Otherwise, [fail] will be called
*/
-fun String.fbRequest(fail: () -> Unit = {}, action: RequestAuth.() -> Unit) {
+fun String?.fbRequest(fail: () -> Unit = {}, action: RequestAuth.() -> Unit) {
+ if (this == null) return fail()
val savedAuth = authMap[this]
if (savedAuth != null) {
savedAuth.action()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Menu.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Menu.kt
new file mode 100644
index 00000000..59f87fbd
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Menu.kt
@@ -0,0 +1,158 @@
+package com.pitchedapps.frost.facebook.requests
+
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.MapperFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.pitchedapps.frost.facebook.FB_URL_BASE
+import com.pitchedapps.frost.facebook.formattedFbUrl
+import com.pitchedapps.frost.utils.L
+import okhttp3.Call
+import org.apache.commons.text.StringEscapeUtils
+import org.jsoup.Jsoup
+import java.io.IOException
+
+/**
+ * Created by Allan Wang on 29/12/17.
+ */
+fun RequestAuth.getMenuData(): FrostRequest<MenuData?> {
+
+ val body = listOf(
+ "fb_dtsg" to fb_dtsg,
+ "__user" to userId
+ ).withEmptyData("m_sess", "__dyn", "__req", "__ajax__")
+
+ return frostRequest(::parseMenu) {
+ url("${FB_URL_BASE}bookmarks/flyout/body/?id=u_0_2")
+ post(body.toForm())
+ }
+
+}
+
+fun parseMenu(call: Call): MenuData? {
+ val fullString = call.execute().body()?.string() ?: return null
+ var jsonString = fullString.substringAfter("bookmarkGroups", "")
+ .substringAfter("[", "")
+
+ if (jsonString.isBlank()) return null
+
+ jsonString = "{ \"data\" : [${StringEscapeUtils.unescapeEcmaScript(jsonString)}"
+
+ val mapper = ObjectMapper()
+ .disable(MapperFeature.AUTO_DETECT_SETTERS)
+
+ return try {
+ val data = mapper.readValue(jsonString, MenuData::class.java)
+
+ // parse footer content
+
+ val footer = fullString.substringAfter("footerMarkup", "")
+ .substringAfter("{", "")
+ .substringBefore("}", "")
+
+ val doc = Jsoup.parseBodyFragment(StringEscapeUtils.unescapeEcmaScript(
+ StringEscapeUtils.unescapeEcmaScript(footer)))
+ val footerData = mutableListOf<MenuFooterItem>()
+ val footerSmallData = mutableListOf<MenuFooterItem>()
+
+ doc.select("a[href]").forEach {
+ val text = it.text()
+ it.parent()
+ if (text.isEmpty()) return@forEach
+ val href = it.attr("href").formattedFbUrl
+ val item = MenuFooterItem(name = text, url = href)
+ if (it.parent().tag().name == "span")
+ footerSmallData.add(item)
+ else
+ footerData.add(item)
+ }
+
+ return data.copy(footer = MenuFooter(footerData, footerSmallData))
+ } catch (e: IOException) {
+ L.e(e, "Menu parse fail")
+ null
+ }
+}
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class MenuData(val data: List<MenuHeader> = emptyList(),
+ val footer: MenuFooter = MenuFooter()) {
+
+ @JsonCreator constructor(
+ @JsonProperty("data") data: List<MenuHeader>?
+ ) : this(data ?: emptyList(), MenuFooter())
+
+ fun flatMapValid() : List<MenuItemData> {
+ val items = mutableListOf<MenuItemData>()
+ data.forEach {
+ if (it.isValid) items.add(it)
+ items.addAll(it.visible.filter(MenuItem::isValid))
+ }
+
+ items.addAll(footer.data.filter(MenuFooterItem::isValid))
+ items.addAll(footer.smallData.filter(MenuFooterItem::isValid))
+
+ return items
+ }
+
+}
+
+interface MenuItemData {
+ val isValid: Boolean
+}
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class MenuHeader(val id: String? = null,
+ val header: String? = null,
+ val visible: List<MenuItem> = emptyList(),
+ val all: List<MenuItem> = emptyList()) : MenuItemData {
+
+ @JsonCreator constructor(
+ @JsonProperty("id") id: String?,
+ @JsonProperty("header") header: String?,
+ @JsonProperty("visible") visible: List<MenuItem>?,
+ @JsonProperty("all") all: List<MenuItem>?,
+ @JsonProperty("fake") fake: Boolean?
+ ) : this(id, header, visible ?: emptyList(), all ?: emptyList())
+
+ override val isValid: Boolean
+ get() = header != null
+}
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class MenuItem(val id: String? = null,
+ val name: String? = null,
+ val pic: String? = null,
+ val url: String? = null,
+ val count: Int = 0,
+ val countDetails: String? = null) : MenuItemData {
+
+ @JsonCreator constructor(
+ @JsonProperty("id") id: String?,
+ @JsonProperty("name") name: String?,
+ @JsonProperty("pic") pic: String?,
+ @JsonProperty("url") url: String?,
+ @JsonProperty("count") count: Int?,
+ @JsonProperty("count_details") countDetails: String?,
+ @JsonProperty("fake") fake: Boolean?
+ ) : this(id, name, pic?.formattedFbUrl, url?.formattedFbUrl, count ?: 0, countDetails)
+
+ override val isValid: Boolean
+ get() = name != null && url != null
+}
+
+data class MenuFooter(val data: List<MenuFooterItem> = emptyList(),
+ val smallData: List<MenuFooterItem> = emptyList()) {
+
+ val hasContent
+ get() = data.isNotEmpty() || smallData.isNotEmpty()
+
+}
+
+data class MenuFooterItem(val name: String? = null,
+ val url: String? = null,
+ val isSmall: Boolean = false) : MenuItemData {
+ override val isValid: Boolean
+ get() = name != null && url != null
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
index 963d00bb..8ab775e0 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
@@ -6,28 +6,15 @@ import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import ca.allanwang.kau.adapters.fastAdapter
import ca.allanwang.kau.utils.withArguments
-import com.mikepenz.fastadapter.FastAdapter
-import com.mikepenz.fastadapter.IItem
-import com.mikepenz.fastadapter.adapters.ItemAdapter
-import com.mikepenz.fastadapter_extensions.items.ProgressItem
-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.FbCookie
import com.pitchedapps.frost.facebook.FbItem
-import com.pitchedapps.frost.parsers.FrostParser
-import com.pitchedapps.frost.parsers.ParseResponse
import com.pitchedapps.frost.utils.*
-import com.pitchedapps.frost.views.FrostRecyclerView
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
-import org.jetbrains.anko.doAsync
-import org.jetbrains.anko.toast
-import org.jetbrains.anko.uiThread
/**
* Created by Allan Wang on 2017-11-07.
@@ -39,9 +26,10 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
companion object {
private const val ARG_POSITION = "arg_position"
+ private const val ARG_VALID = "arg_valid"
- internal operator fun invoke(base: () -> BaseFragment, data: FbItem, position: Int): BaseFragment {
- val fragment = if (Prefs.nativeViews) base() else WebFragment()
+ internal operator fun invoke(base: () -> BaseFragment, useFallback: Boolean, data: FbItem, position: Int): BaseFragment {
+ val fragment = if (!useFallback) base() else WebFragment()
val d = if (data == FbItem.FEED) FeedSort(Prefs.feedSort).item else data
fragment.withArguments(
ARG_URL to d.url,
@@ -56,6 +44,17 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
override val baseEnum: FbItem by lazy { FbItem[arguments]!! }
override val position: Int by lazy { arguments!!.getInt(ARG_POSITION) }
+ override var valid: Boolean
+ get() = arguments!!.getBoolean(ARG_VALID, true)
+ set(value) {
+ if (value || this is WebFragment) return
+ arguments!!.putBoolean(ARG_VALID, value)
+ L.e("Invalidating position $position")
+ frostAnswersCustom("Native Fallback",
+ "Item" to baseEnum.name)
+ (context as MainActivityContract).reloadFragment(this)
+ }
+
override var firstLoad: Boolean = true
private var activityDisposable: Disposable? = null
private var onCreateRunnable: ((FragmentContract) -> Unit)? = null
@@ -147,6 +146,7 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
}
override fun onDestroyView() {
+ L.i("Fragment on destroy $position ${hashCode()}")
content?.destroy()
content = null
super.onDestroyView()
@@ -175,92 +175,3 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
override fun onTabClick(): Unit = content?.core?.onTabClicked() ?: Unit
}
-abstract class RecyclerFragment<T : Any, 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>
-
- open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url)
-
- val adapter: ItemAdapter<Item> = ItemAdapter()
-
- abstract fun toItems(response: ParseResponse<T>): List<Item>
-
- override final fun bind(recyclerView: FrostRecyclerView) {
- recyclerView.adapter = getAdapter()
- recyclerView.onReloadClear = { adapter.clear() }
- bindImpl(recyclerView)
- }
-
- override fun firstLoadRequest() {
- val core = core ?: return
- if (firstLoad) {
- core.reloadBase(true)
- firstLoad = false
- }
- }
-
- /**
- * Anything to call for one time bindings
- * At this stage, all adapters will have FastAdapter references
- */
- open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
-
- /**
- * Create the fast adapter to bind to the recyclerview
- */
- open fun getAdapter(): FastAdapter<IItem<*, *>> = fastAdapter(this.adapter)
-
- override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
- doAsync {
- progress(10)
- val cookie = FbCookie.webCookie
- val doc = getDoc(cookie)
- progress(60)
- val response = parser.parse(cookie, doc)
- if (response == null) {
- uiThread { 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(response)
- progress(97)
- uiThread { adapter.setNewList(items) }
- callback(true)
- }
- }
-}
-
-//abstract class PagedRecyclerFragment<T : Any, Item : IItem<*, *>> : RecyclerFragment<T, Item>() {
-//
-// var allowPagedLoading = true
-//
-// val footerAdapter = ItemAdapter<FrostProgress>()
-//
-// val footerScrollListener = object : EndlessRecyclerOnScrollListener(footerAdapter) {
-// override fun onLoadMore(currentPage: Int) {
-// TODO("not implemented")
-//
-// }
-//
-// }
-//
-// override fun getAdapter() = fastAdapter(adapter, footerAdapter)
-//
-// override fun bindImpl(recyclerView: FrostRecyclerView) {
-// recyclerView.addOnScrollListener(footerScrollListener)
-// }
-//
-// override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
-// footerScrollListener.
-// super.reload(progress, callback)
-// }
-//}
-
-class FrostProgress : ProgressItem() \ 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
index a78eb0d0..98a081e6 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
@@ -16,6 +16,13 @@ interface FragmentContract : FrostContentContainer {
val content: FrostContentParent?
/**
+ * Defines whether the fragment is valid in the viewpager
+ * Or if it needs to be recreated
+ * May be called from any thread to toggle status
+ */
+ var valid: Boolean
+
+ /**
* Helper to retrieve the core from [content]
*/
val core: FrostContentCore?
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt
new file mode 100644
index 00000000..c490de60
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt
@@ -0,0 +1,149 @@
+package com.pitchedapps.frost.fragments
+
+import ca.allanwang.kau.adapters.fastAdapter
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.adapters.ItemAdapter
+import com.mikepenz.fastadapter.adapters.ModelAdapter
+import com.mikepenz.fastadapter_extensions.items.ProgressItem
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.facebook.FbCookie
+import com.pitchedapps.frost.parsers.FrostParser
+import com.pitchedapps.frost.parsers.ParseResponse
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.frostJsoup
+import com.pitchedapps.frost.views.FrostRecyclerView
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.toast
+import org.jetbrains.anko.uiThread
+
+/**
+ * Created by Allan Wang on 27/12/17.
+ */
+abstract class RecyclerFragment : BaseFragment(), RecyclerContentContract {
+
+ override val layoutRes: Int = R.layout.view_content_recycler
+
+ override fun firstLoadRequest() {
+ val core = core ?: return
+ if (firstLoad) {
+ core.reloadBase(true)
+ firstLoad = false
+ }
+ }
+
+ override final fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
+ reloadImpl(progress) {
+ if (it)
+ callback(it)
+ else
+ valid = false
+ }
+ }
+
+ protected abstract fun reloadImpl(progress: (Int) -> Unit, callback: (Boolean) -> Unit)
+}
+
+abstract class GenericRecyclerFragment<T, Item : IItem<*, *>> : RecyclerFragment() {
+
+ abstract fun mapper(data: T): Item
+
+ val adapter: ModelAdapter<T, Item> = ModelAdapter(this::mapper)
+
+ override final fun bind(recyclerView: FrostRecyclerView) {
+ recyclerView.adapter = getAdapter()
+ recyclerView.onReloadClear = { adapter.clear() }
+ bindImpl(recyclerView)
+ }
+
+ /**
+ * Anything to call for one time bindings
+ * At this stage, all adapters will have FastAdapter references
+ */
+ open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
+
+ /**
+ * Create the fast adapter to bind to the recyclerview
+ */
+ open fun getAdapter(): FastAdapter<IItem<*, *>> = fastAdapter(this.adapter)
+
+}
+
+abstract class FrostParserFragment<T : Any, Item : IItem<*, *>> : RecyclerFragment() {
+
+ /**
+ * The parser to make this all happen
+ */
+ abstract val parser: FrostParser<T>
+
+ open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url)
+
+ abstract fun toItems(response: ParseResponse<T>): List<Item>
+
+ val adapter: ItemAdapter<Item> = ItemAdapter()
+
+ override final fun bind(recyclerView: FrostRecyclerView) {
+ recyclerView.adapter = getAdapter()
+ recyclerView.onReloadClear = { adapter.clear() }
+ bindImpl(recyclerView)
+ }
+
+ /**
+ * Anything to call for one time bindings
+ * At this stage, all adapters will have FastAdapter references
+ */
+ open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
+
+ /**
+ * Create the fast adapter to bind to the recyclerview
+ */
+ open fun getAdapter(): FastAdapter<IItem<*, *>> = fastAdapter(this.adapter)
+
+ override fun reloadImpl(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
+ doAsync {
+ progress(10)
+ val cookie = FbCookie.webCookie
+ val doc = getDoc(cookie)
+ progress(60)
+ val response = parser.parse(cookie, doc)
+ if (response == null) {
+ L.eThrow("RecyclerFragment failed for ${baseEnum.name}")
+ return@doAsync callback(false)
+ }
+ progress(80)
+ val items = toItems(response)
+ progress(97)
+ uiThread { adapter.setNewList(items) }
+ callback(true)
+ }
+ }
+}
+
+//abstract class PagedRecyclerFragment<T : Any, Item : IItem<*, *>> : RecyclerFragment<T, Item>() {
+//
+// var allowPagedLoading = true
+//
+// val footerAdapter = ItemAdapter<FrostProgress>()
+//
+// val footerScrollListener = object : EndlessRecyclerOnScrollListener(footerAdapter) {
+// override fun onLoadMore(currentPage: Int) {
+// TODO("not implemented")
+//
+// }
+//
+// }
+//
+// override fun getAdapter() = fastAdapter(adapter, footerAdapter)
+//
+// override fun bindImpl(recyclerView: FrostRecyclerView) {
+// recyclerView.addOnScrollListener(footerScrollListener)
+// }
+//
+// override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
+// footerScrollListener.
+// super.reload(progress, callback)
+// }
+//}
+
+class FrostProgress : ProgressItem()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt
index 4d4a6f8b..ca2912e8 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt
@@ -1,17 +1,22 @@
package com.pitchedapps.frost.fragments
+import com.mikepenz.fastadapter.IItem
+import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
-import com.pitchedapps.frost.iitems.NotificationIItem
+import com.pitchedapps.frost.facebook.requests.*
+import com.pitchedapps.frost.iitems.*
import com.pitchedapps.frost.parsers.FrostNotifs
import com.pitchedapps.frost.parsers.NotifParser
import com.pitchedapps.frost.parsers.ParseResponse
import com.pitchedapps.frost.utils.frostJsoup
import com.pitchedapps.frost.views.FrostRecyclerView
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.uiThread
/**
* Created by Allan Wang on 27/12/17.
*/
-class NotificationFragment : RecyclerFragment<FrostNotifs, NotificationIItem>() {
+class NotificationFragment : FrostParserFragment<FrostNotifs, NotificationIItem>() {
override val parser = NotifParser
@@ -23,5 +28,37 @@ class NotificationFragment : RecyclerFragment<FrostNotifs, NotificationIItem>()
override fun bindImpl(recyclerView: FrostRecyclerView) {
NotificationIItem.bindEvents(adapter)
}
+}
+class MenuFragment : GenericRecyclerFragment<MenuItemData, IItem<*, *>>() {
+
+ override fun mapper(data: MenuItemData): IItem<*, *> = when (data) {
+ is MenuHeader -> MenuHeaderIItem(data)
+ is MenuItem -> MenuContentIItem(data)
+ is MenuFooterItem ->
+ if (data.isSmall) MenuFooterSmallIItem(data)
+ else MenuFooterIItem(data)
+ else -> throw IllegalArgumentException("Menu item in fragment has invalid type ${data::class.java.simpleName}")
+ }
+
+ override fun bindImpl(recyclerView: FrostRecyclerView) {
+ ClickableIItemContract.bindEvents(adapter)
+ }
+
+ override fun reloadImpl(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
+ doAsync {
+ val cookie = FbCookie.webCookie
+ progress(10)
+ cookie.fbRequest({ callback(false) }) {
+ progress(30)
+ val data = getMenuData().invoke() ?: return@fbRequest callback(false)
+ if (data.data.isEmpty()) return@fbRequest callback(false)
+ progress(70)
+ val items = data.flatMapValid()
+ progress(90)
+ uiThread { adapter.add(items) }
+ callback(true)
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt
index 2740a36f..cdeea064 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt
@@ -1,26 +1,27 @@
package com.pitchedapps.frost.fragments
import com.pitchedapps.frost.R
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.views.FrostWebView
import com.pitchedapps.frost.web.FrostWebViewClient
import com.pitchedapps.frost.web.FrostWebViewClientMenu
/**
* Created by Allan Wang on 27/12/17.
+ *
+ * Basic webfragment
+ * Do not extend as this is always a fallback
*/
-open class WebFragment : BaseFragment() {
+class WebFragment : BaseFragment() {
override val layoutRes: Int = R.layout.view_content_web
/**
* Given a webview, output a client
*/
- open fun client(web: FrostWebView) = FrostWebViewClient(web)
-
-}
-
-class WebFragmentMenu : WebFragment() {
-
- override fun client(web: FrostWebView) = FrostWebViewClientMenu(web)
+ fun client(web: FrostWebView) = when (baseEnum) {
+ FbItem.MENU -> FrostWebViewClientMenu(web)
+ else -> FrostWebViewClient(web)
+ }
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/GenericIItems.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/GenericIItems.kt
new file mode 100644
index 00000000..625ecff9
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/GenericIItems.kt
@@ -0,0 +1,97 @@
+package com.pitchedapps.frost.iitems
+
+import android.content.Context
+import android.view.View
+import android.widget.TextView
+import ca.allanwang.kau.iitems.KauIItem
+import ca.allanwang.kau.ui.createSimpleRippleDrawable
+import ca.allanwang.kau.utils.bindView
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.IAdapter
+import com.mikepenz.fastadapter.IItem
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.launchWebOverlay
+
+/**
+ * Created by Allan Wang on 30/12/17.
+ */
+
+/**
+ * Base contract for anything with a url that may be launched in a new overlay
+ */
+interface ClickableIItemContract {
+
+ val url: String?
+
+ fun click(context: Context) {
+ val url = url ?: return
+ context.launchWebOverlay(url)
+ }
+
+ companion object {
+ fun bindEvents(adapter: IAdapter<IItem<*, *>>) {
+ adapter.fastAdapter.withSelectable(false)
+ .withOnClickListener { v, _, item, _ ->
+ if (item is ClickableIItemContract) {
+ item.click(v.context)
+ true
+ } else
+ false
+ }
+ }
+ }
+
+}
+
+/**
+ * Generic header item
+ * Not clickable with an accent color
+ */
+open class HeaderIItem(val text: String?,
+ itemId: Int = R.layout.iitem_header)
+ : KauIItem<HeaderIItem, HeaderIItem.ViewHolder>(R.layout.iitem_header, ::ViewHolder, itemId) {
+
+ class ViewHolder(itemView: View) : FastAdapter.ViewHolder<HeaderIItem>(itemView) {
+
+ val text: TextView by bindView(R.id.item_header_text)
+
+ override fun bindView(item: HeaderIItem, payloads: MutableList<Any>) {
+ text.setTextColor(Prefs.accentColor)
+ text.text = item.text
+ text.setBackgroundColor(Prefs.nativeBgColor)
+ }
+
+ override fun unbindView(item: HeaderIItem) {
+ text.text = null
+ }
+ }
+
+}
+
+/**
+ * Generic text item
+ * Clickable with text color
+ */
+open class TextIItem(val text: String?,
+ override val url: String?,
+ itemId: Int = R.layout.iitem_text)
+ : KauIItem<TextIItem, TextIItem.ViewHolder>(R.layout.iitem_text, ::ViewHolder, itemId),
+ ClickableIItemContract {
+
+ class ViewHolder(itemView: View) : FastAdapter.ViewHolder<TextIItem>(itemView) {
+
+ val text: TextView by bindView(R.id.item_text_view)
+
+ override fun bindView(item: TextIItem, payloads: MutableList<Any>) {
+ text.setTextColor(Prefs.textColor)
+ text.text = item.text
+ text.background = createSimpleRippleDrawable(Prefs.bgColor, Prefs.nativeBgColor)
+ }
+
+ override fun unbindView(item: TextIItem) {
+ text.text = null
+ }
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt
new file mode 100644
index 00000000..690d1be8
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt
@@ -0,0 +1,67 @@
+package com.pitchedapps.frost.iitems
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import ca.allanwang.kau.iitems.KauIItem
+import ca.allanwang.kau.ui.createSimpleRippleDrawable
+import ca.allanwang.kau.utils.bindView
+import ca.allanwang.kau.utils.gone
+import ca.allanwang.kau.utils.visible
+import com.bumptech.glide.Glide
+import com.mikepenz.fastadapter.FastAdapter
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.facebook.requests.MenuFooterItem
+import com.pitchedapps.frost.facebook.requests.MenuHeader
+import com.pitchedapps.frost.facebook.requests.MenuItem
+import com.pitchedapps.frost.glide.FrostGlide
+import com.pitchedapps.frost.glide.transform
+import com.pitchedapps.frost.utils.Prefs
+
+/**
+ * Created by Allan Wang on 30/12/17.
+ */
+class MenuContentIItem(val data: MenuItem)
+ : KauIItem<MenuContentIItem, MenuContentIItem.ViewHolder>(R.layout.iitem_menu, ::ViewHolder),
+ ClickableIItemContract {
+
+ override val url: String?
+ get() = data.url
+
+ class ViewHolder(itemView: View) : FastAdapter.ViewHolder<MenuContentIItem>(itemView) {
+
+ val frame: ViewGroup by bindView(R.id.item_frame)
+ val icon: ImageView by bindView(R.id.item_icon)
+ val content: TextView by bindView(R.id.item_content)
+ val badge: TextView by bindView(R.id.item_badge)
+
+ override fun bindView(item: MenuContentIItem, payloads: MutableList<Any>) {
+ frame.background = createSimpleRippleDrawable(Prefs.textColor, Prefs.nativeBgColor)
+ content.setTextColor(Prefs.textColor)
+ badge.setTextColor(Prefs.textColor)
+ val iconUrl = item.data.pic
+ if (iconUrl != null)
+ Glide.with(itemView).load(iconUrl)
+ .transform(FrostGlide.roundCorner)
+ .into(icon.visible())
+ else
+ icon.gone()
+ content.text = item.data.name
+ }
+
+ override fun unbindView(item: MenuContentIItem) {
+ badge.gone()
+ }
+ }
+}
+
+class MenuHeaderIItem(val data: MenuHeader) : HeaderIItem(data.header,
+ itemId = R.id.item_menu_header)
+
+class MenuFooterIItem(val data: MenuFooterItem)
+ : TextIItem(data.name, data.url, R.id.item_menu_footer)
+
+class MenuFooterSmallIItem(val data: MenuFooterItem)
+ : TextIItem(data.name, data.url, R.id.item_menu_footer_small)
+
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt
index c7f61351..a16b0224 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt
@@ -54,8 +54,7 @@ class NotificationIItem(val notification: FrostNotif, val cookie: String) : KauI
override fun bindView(item: NotificationIItem, payloads: MutableList<Any>) {
val notif = item.notification
frame.background = createSimpleRippleDrawable(Prefs.textColor,
- Prefs.bgColor.colorToForeground(if (notif.unread) 0.7f else 0.0f)
- .withAlpha(30))
+ Prefs.nativeBgColor(notif.unread))
content.setTextColor(Prefs.textColor)
date.setTextColor(Prefs.textColor.withAlpha(150))
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt
index 451eb774..23852852 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt
@@ -64,7 +64,8 @@ private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
override fun parseImpl(doc: Document): FrostNotifs? {
val notificationList = doc.getElementById("notifications_list") ?: return null
- val notifications = notificationList.getElementsByAttributeValueContaining("id", "list_notif_")
+ val notifications = notificationList
+ .getElementsByAttributeValueContaining("id", "list_notif_")
.mapNotNull(this::parseNotif)
val seeMore = parseLink(doc.getElementsByAttributeValue("href", "/notifications.php?more").first())
return FrostNotifs(notifications, seeMore)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
index f14039b7..7bec8ce0 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
@@ -5,7 +5,9 @@ import ca.allanwang.kau.kotlin.lazyResettable
import ca.allanwang.kau.kpref.KPref
import ca.allanwang.kau.kpref.StringSet
import ca.allanwang.kau.kpref.kpref
+import ca.allanwang.kau.utils.colorToForeground
import ca.allanwang.kau.utils.isColorVisibleOn
+import ca.allanwang.kau.utils.withAlpha
import com.pitchedapps.frost.enums.FACEBOOK_BLUE
import com.pitchedapps.frost.enums.FeedSort
import com.pitchedapps.frost.enums.MainActivityLayout
@@ -59,11 +61,18 @@ object Prefs : KPref() {
val accentColor: Int
get() = t.accentColor
- val accentColorForWhite: Int
+ inline val accentColorForWhite: Int
get() = if (accentColor.isColorVisibleOn(Color.WHITE)) accentColor
else if (textColor.isColorVisibleOn(Color.WHITE)) textColor
else FACEBOOK_BLUE
+ inline val nativeBgColor: Int
+ get() = Prefs.bgColor.withAlpha(30)
+
+ fun nativeBgColor(unread: Boolean) = Prefs.bgColor
+ .colorToForeground(if (unread) 0.7f else 0.0f)
+ .withAlpha(30)
+
val bgColor: Int
get() = t.bgColor
@@ -79,8 +88,8 @@ object Prefs : KPref() {
val isCustomTheme: Boolean
get() = t == Theme.CUSTOM
- val frostId: String
- get() = "${installDate}-${identifier}"
+ inline val frostId: String
+ get() = "$installDate-$identifier"
var tintNavBar: Boolean by kpref("tint_nav_bar", true)
@@ -150,10 +159,8 @@ object Prefs : KPref() {
var mainActivityLayoutType: Int by kpref("main_activity_layout_type", 0)
- val mainActivityLayout: MainActivityLayout
+ inline val mainActivityLayout: MainActivityLayout
get() = MainActivityLayout(mainActivityLayoutType)
- var nativeViews: Boolean by kpref("native_views", true)
-
override fun deleteKeys() = arrayOf("search_bar")
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
index 344fcb27..960fe4c2 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
@@ -55,7 +55,7 @@ class FrostChromeClient(web: FrostWebView) : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
if (consoleBlacklist.any { consoleMessage.message().contains(it) }) return true
- L.d("Chrome Console ${consoleMessage.lineNumber()}: ${consoleMessage.message()}")
+ L.v("Chrome Console ${consoleMessage.lineNumber()}: ${consoleMessage.message()}")
return true
}