1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-09 20:42:34 +01:00

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
This commit is contained in:
Allan Wang 2017-12-31 00:42:49 -05:00 committed by GitHub
parent 041bafccea
commit 3076d9a97c
26 changed files with 716 additions and 162 deletions

View File

@ -172,6 +172,8 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:${GLIDE}"
implementation "com.fasterxml.jackson.core:jackson-databind:2.9.3"
//noinspection GradleDependency
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${LEAK_CANARY}"
//noinspection GradleDependency

View File

@ -28,4 +28,13 @@
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# Jackson
-keep @com.fasterxml.jackson.annotation.JsonIgnoreProperties class * { *; }
-keep @com.fasterxml.jackson.annotation.JsonCreator class * { *; }
-keep @com.fasterxml.jackson.annotation.JsonValue class * { *; }
-keep class com.fasterxml.** { *; }
-keepnames class com.fasterxml.jackson.** { *; }
-keepclassmembers public final enum com.fasterxml.jackson.annotation.JsonAutoDetect$Visibility {
public static final com.fasterxml.jackson.annotation.JsonAutoDetect$Visibility *;
}

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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),

View File

@ -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()

View File

@ -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
}

View File

@ -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()

View File

@ -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]
*/

View File

@ -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()

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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")
}

View File

@ -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
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_header_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/kau_activity_vertical_margin"
android:paddingEnd="@dimen/kau_padding_small"
android:paddingStart="@dimen/kau_activity_horizontal_margin"
android:paddingTop="@dimen/kau_padding_small" />

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/item_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/kau_padding_small"
android:paddingStart="@dimen/kau_activity_horizontal_margin"
android:paddingTop="@dimen/kau_padding_small">
<ImageView
android:id="@+id/item_icon"
android:layout_width="36dp"
android:layout_height="36dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5" />
<TextView
android:id="@+id/item_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/kau_activity_horizontal_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5" />
<TextView
android:id="@+id/item_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/kau_activity_horizontal_margin"
android:layout_marginStart="@dimen/kau_activity_horizontal_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/item_badge"
app:layout_constraintStart_toEndOf="@id/item_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:paddingBottom="@dimen/kau_activity_vertical_margin"
android:paddingEnd="@dimen/kau_padding_small"
android:paddingStart="@dimen/kau_activity_horizontal_margin"
android:paddingTop="@dimen/kau_padding_small" />

View File

@ -3,6 +3,9 @@
<item name="item_account" type="id" />
<item name="item_keyword" type="id" />
<item name="item_about_links" type="id" />
<item name="item_menu_header" type="id" />
<item name="item_menu_footer" type="id" />
<item name="item_menu_footer_small" type="id" />
<item name="intro_phone" type="id" />
<item name="intro_phone_screen" type="id" />

View File

@ -6,13 +6,19 @@
<item text="" />
-->
<version title="v1.7.4" />
<item text="Mark notifications as read when clicked!" />
<item text="Create menu parser" />
<item text="Implement automatic web fallback" />
<item text="" />
<item text="" />
<item text="" />
<version title="v1.7.2" />
<item text="Optimize login view" />
<item text="Rewrite parsers" />
<item text="Fix message notification icons" />
<item text="Small theme updates" />
<item text="" />
<item text="" />
<version title="v1.7.1" />
<item text="Fix launching messages in new overlay" />

View File

@ -1,7 +1,9 @@
package com.pitchedapps.frost.facebook
import com.fasterxml.jackson.databind.ObjectMapper
import com.pitchedapps.frost.facebook.requests.getAuth
import com.pitchedapps.frost.facebook.requests.getFullSizedImage
import com.pitchedapps.frost.facebook.requests.getMenuData
import com.pitchedapps.frost.facebook.requests.markNotificationRead
import com.pitchedapps.frost.internal.AUTH
import com.pitchedapps.frost.internal.COOKIE
@ -59,4 +61,13 @@ class FbRequestTest {
assertTrue(url?.startsWith("https://scontent") == true)
}
}
@Test
fun testMenu() {
val data = AUTH.getMenuData().invoke()
assertNotNull(data)
println(ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(data!!))
assertTrue(data.footer.hasContent, "Footer may be badly parsed")
val items = data.flatMapValid()
assertTrue(items.size > 15, "Something may be badly parsed")
}
}

View File

@ -1,5 +1,10 @@
# Changelog
## v1.7.4
* Mark notifications as read when clicked!
* Create menu parser
* Implement automatic web fallback
## v1.7.2
* Optimize login view
* Rewrite parsers