From 3076d9a97c203497aec1415d8ac6037d10eebb46 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 31 Dec 2017 00:42:49 -0500 Subject: feature/menu-parser (#582) * Test menu parser * Add menu fragment implementation * Test proguard * Clean up * Use async * Use invoke * Try without proguard * Try 2 * Add fallback logic * Use normal notification event * Add custom event flag * Add rest of menu fragment data * Ensure fallback works * Update docs --- .../frost/activities/BaseMainActivity.kt | 64 ++++++--- .../pitchedapps/frost/activities/MainActivity.kt | 16 +-- .../frost/contracts/ActivityContract.kt | 2 + .../com/pitchedapps/frost/facebook/FbItem.kt | 7 +- .../frost/facebook/requests/FbRequest.kt | 3 +- .../pitchedapps/frost/facebook/requests/Menu.kt | 158 +++++++++++++++++++++ .../pitchedapps/frost/fragments/FragmentBase.kt | 119 ++-------------- .../frost/fragments/FragmentContract.kt | 7 + .../frost/fragments/RecyclerFragmentBase.kt | 149 +++++++++++++++++++ .../frost/fragments/RecyclerFragments.kt | 41 +++++- .../pitchedapps/frost/fragments/WebFragments.kt | 17 +-- .../com/pitchedapps/frost/iitems/GenericIItems.kt | 97 +++++++++++++ .../com/pitchedapps/frost/iitems/MenuIItem.kt | 67 +++++++++ .../pitchedapps/frost/iitems/NotificationIItem.kt | 3 +- .../com/pitchedapps/frost/parsers/NotifParser.kt | 3 +- .../kotlin/com/pitchedapps/frost/utils/Prefs.kt | 19 ++- .../pitchedapps/frost/web/FrostChromeClients.kt | 2 +- 17 files changed, 615 insertions(+), 159 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Menu.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/iitems/GenericIItems.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt (limited to 'app/src/main/kotlin') 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) : FragmentPagerAdapter(fm) { + override fun reloadFragment(fragment: BaseFragment) { + runOnUiThread { adapter.reloadFragment(fragment) } + } + + inner class SectionsPagerAdapter(val pages: List) : FragmentPagerAdapter(supportFragmentManager) { + + val forcedFallbacks = mutableSetOf() + + 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()!! var lastPosition = -1 - val headerBadgeObservable = PublishSubject.create() - 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()!! 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 = 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 { + + 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() + val footerSmallData = mutableListOf() + + 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 = emptyList(), + val footer: MenuFooter = MenuFooter()) { + + @JsonCreator constructor( + @JsonProperty("data") data: List? + ) : this(data ?: emptyList(), MenuFooter()) + + fun flatMapValid() : List { + val items = mutableListOf() + 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 = emptyList(), + val all: List = emptyList()) : MenuItemData { + + @JsonCreator constructor( + @JsonProperty("id") id: String?, + @JsonProperty("header") header: String?, + @JsonProperty("visible") visible: List?, + @JsonProperty("all") all: List?, + @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 = emptyList(), + val smallData: List = 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> : BaseFragment(), RecyclerContentContract { - - override val layoutRes: Int = R.layout.view_content_recycler - - /** - * The parser to make this all happen - */ - abstract val parser: FrostParser - - open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url) - - val adapter: ItemAdapter = ItemAdapter() - - abstract fun toItems(response: ParseResponse): List - - 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> = 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> : RecyclerFragment() { -// -// var allowPagedLoading = true -// -// val footerAdapter = ItemAdapter() -// -// 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 @@ -15,6 +15,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] */ 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> : RecyclerFragment() { + + abstract fun mapper(data: T): Item + + val adapter: ModelAdapter = 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> = fastAdapter(this.adapter) + +} + +abstract class FrostParserFragment> : RecyclerFragment() { + + /** + * The parser to make this all happen + */ + abstract val parser: FrostParser + + open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url) + + abstract fun toItems(response: ParseResponse): List + + val adapter: ItemAdapter = 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> = 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> : RecyclerFragment() { +// +// var allowPagedLoading = true +// +// val footerAdapter = ItemAdapter() +// +// 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() { +class NotificationFragment : FrostParserFragment() { override val parser = NotifParser @@ -23,5 +28,37 @@ class NotificationFragment : RecyclerFragment() override fun bindImpl(recyclerView: FrostRecyclerView) { NotificationIItem.bindEvents(adapter) } +} +class MenuFragment : GenericRecyclerFragment>() { + + 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>) { + 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(R.layout.iitem_header, ::ViewHolder, itemId) { + + class ViewHolder(itemView: View) : FastAdapter.ViewHolder(itemView) { + + val text: TextView by bindView(R.id.item_header_text) + + override fun bindView(item: HeaderIItem, payloads: MutableList) { + 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(R.layout.iitem_text, ::ViewHolder, itemId), + ClickableIItemContract { + + class ViewHolder(itemView: View) : FastAdapter.ViewHolder(itemView) { + + val text: TextView by bindView(R.id.item_text_view) + + override fun bindView(item: TextIItem, payloads: MutableList) { + 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(R.layout.iitem_menu, ::ViewHolder), + ClickableIItemContract { + + override val url: String? + get() = data.url + + class ViewHolder(itemView: View) : FastAdapter.ViewHolder(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) { + 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) { 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(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 } -- cgit v1.2.3