1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 20:12:39 +01:00

Enhancement/fragment interface (#564)

* Begin fragment interfaces and themable contracts

* Prepare swiperefresh interface

* Snapshot

* Add compilable version

* Revamp once more

* Finalize layouts

* Cleanup
This commit is contained in:
Allan Wang 2017-12-21 02:16:34 -05:00 committed by GitHub
parent 82f9aca964
commit d683cae6ff
44 changed files with 1728 additions and 975 deletions

View File

@ -0,0 +1,402 @@
package com.pitchedapps.frost.activities
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.PointF
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.support.annotation.StringRes
import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.TabLayout
import android.support.v4.app.ActivityOptionsCompat
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
import android.view.MenuItem
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.widget.FrameLayout
import ca.allanwang.kau.searchview.SearchItem
import ca.allanwang.kau.searchview.SearchView
import ca.allanwang.kau.searchview.SearchViewHolder
import ca.allanwang.kau.searchview.bindSearchView
import ca.allanwang.kau.utils.*
import co.zsmb.materialdrawerkt.builders.Builder
import co.zsmb.materialdrawerkt.builders.accountHeader
import co.zsmb.materialdrawerkt.builders.drawer
import co.zsmb.materialdrawerkt.draweritems.badgeable.primaryItem
import co.zsmb.materialdrawerkt.draweritems.badgeable.secondaryItem
import co.zsmb.materialdrawerkt.draweritems.divider
import co.zsmb.materialdrawerkt.draweritems.profile.profile
import co.zsmb.materialdrawerkt.draweritems.profile.profileSetting
import com.crashlytics.android.answers.ContentViewEvent
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.Drawer
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.FileChooserContract
import com.pitchedapps.frost.contracts.FileChooserDelegate
import com.pitchedapps.frost.contracts.MainActivityContract
import com.pitchedapps.frost.contracts.VideoViewHolder
import com.pitchedapps.frost.dbflow.TAB_COUNT
import com.pitchedapps.frost.dbflow.loadFbCookie
import com.pitchedapps.frost.dbflow.loadFbTabs
import com.pitchedapps.frost.enums.MainActivityLayout
import com.pitchedapps.frost.enums.Theme
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.parsers.SearchParser
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.iab.FrostBilling
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import com.pitchedapps.frost.utils.iab.IabMain
import com.pitchedapps.frost.views.BadgedIcon
import com.pitchedapps.frost.views.FrostVideoViewer
import com.pitchedapps.frost.views.FrostViewPager
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
/**
* Created by Allan Wang on 20/12/17.
*
* Most of the logic that is unrelated to handling fragments
*/
abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
FileChooserContract by FileChooserDelegate(),
VideoViewHolder, SearchViewHolder,
FrostBilling by IabMain() {
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)
val fab: FloatingActionButton by bindView(R.id.fab)
val tabs: TabLayout by bindView(R.id.tabs)
val appBar: AppBarLayout by bindView(R.id.appbar)
val coordinator: CoordinatorLayout by bindView(R.id.main_content)
override var videoViewer: FrostVideoViewer? = null
lateinit var drawer: Drawer
lateinit var drawerHeader: AccountHeader
override var searchView: SearchView? = null
private val searchViewCache = mutableMapOf<String, List<SearchItem>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (BuildConfig.VERSION_CODE > Prefs.versionCode) {
Prefs.versionCode = BuildConfig.VERSION_CODE
if (!BuildConfig.DEBUG) {
frostChangelog()
frostAnswersCustom("Version",
"Version code" to BuildConfig.VERSION_CODE,
"Version name" to BuildConfig.VERSION_NAME,
"Build type" to BuildConfig.BUILD_TYPE,
"Frost id" to Prefs.frostId)
}
}
setFrameContentView(Prefs.mainActivityLayout.layoutRes)
setSupportActionBar(toolbar)
adapter = SectionsPagerAdapter(supportFragmentManager, loadFbTabs())
viewPager.adapter = adapter
viewPager.offscreenPageLimit = TAB_COUNT
setupDrawer(savedInstanceState)
// fab.setOnClickListener { view ->
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
// .setAction("Action", null).show()
// }
setFrostColors(toolbar, themeWindow = false, headers = arrayOf(appBar), backgrounds = arrayOf(viewPager))
tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor())
onCreateBilling()
}
fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) {
(0 until tabs.tabCount).asSequence().forEach { i ->
action(i, tabs.getTabAt(i)!!.customView as BadgedIcon)
}
}
private fun setupDrawer(savedInstanceState: Bundle?) {
val navBg = Prefs.bgColor.withMinAlpha(200).toLong()
val navHeader = Prefs.headerColor.withMinAlpha(200)
drawer = drawer {
toolbar = this@BaseMainActivity.toolbar
savedInstance = savedInstanceState
translucentStatusBar = false
sliderBackgroundColor = navBg
drawerHeader = accountHeader {
customViewRes = R.layout.material_drawer_header
textColor = Prefs.iconColor.toLong()
backgroundDrawable = ColorDrawable(navHeader)
selectionSecondLineShown = false
cookies().forEach { (id, name) ->
profile(name = name ?: "") {
iconUrl = PROFILE_PICTURE_URL(id)
textColor = Prefs.textColor.toLong()
selectedTextColor = Prefs.textColor.toLong()
selectedColor = 0x00000001.toLong()
identifier = id
}
}
profileSetting(nameRes = R.string.kau_logout) {
iicon = GoogleMaterial.Icon.gmd_exit_to_app
iconColor = Prefs.textColor.toLong()
textColor = Prefs.textColor.toLong()
identifier = -2L
}
profileSetting(nameRes = R.string.kau_add_account) {
iconDrawable = IconicsDrawable(this@BaseMainActivity, GoogleMaterial.Icon.gmd_add).actionBar().paddingDp(5).color(Prefs.textColor)
textColor = Prefs.textColor.toLong()
identifier = -3L
}
profileSetting(nameRes = R.string.kau_manage_account) {
iicon = GoogleMaterial.Icon.gmd_settings
iconColor = Prefs.textColor.toLong()
textColor = Prefs.textColor.toLong()
identifier = -4L
}
onProfileChanged { _, profile, current ->
if (current) launchWebOverlay(FbItem.PROFILE.url)
else when (profile.identifier) {
-2L -> {
val currentCookie = loadFbCookie(Prefs.userId)
if (currentCookie == null) {
toast(R.string.account_not_found)
FbCookie.reset { launchLogin(cookies(), true) }
} else {
materialDialogThemed {
title(R.string.kau_logout)
content(String.format(string(R.string.kau_logout_confirm_as_x), currentCookie.name ?: Prefs.userId.toString()))
positiveText(R.string.kau_yes)
negativeText(R.string.kau_no)
onPositive { _, _ -> FbCookie.logout(this@BaseMainActivity) }
}
}
}
-3L -> launchNewTask(LoginActivity::class.java, clearStack = false)
-4L -> launchNewTask(SelectorActivity::class.java, cookies(), false)
else -> {
FbCookie.switchUser(profile.identifier, { refreshAll() })
tabsForEachView { _, view -> view.badgeText = null }
}
}
false
}
}
drawerHeader.setActiveProfile(Prefs.userId)
primaryFrostItem(FbItem.FEED_MOST_RECENT)
primaryFrostItem(FbItem.FEED_TOP_STORIES)
primaryFrostItem(FbItem.ACTIVITY_LOG)
divider()
primaryFrostItem(FbItem.PHOTOS)
primaryFrostItem(FbItem.GROUPS)
primaryFrostItem(FbItem.FRIENDS)
primaryFrostItem(FbItem.CHAT)
primaryFrostItem(FbItem.PAGES)
divider()
primaryFrostItem(FbItem.EVENTS)
primaryFrostItem(FbItem.BIRTHDAYS)
primaryFrostItem(FbItem.ON_THIS_DAY)
divider()
primaryFrostItem(FbItem.NOTES)
primaryFrostItem(FbItem.SAVED)
}
}
private fun Builder.primaryFrostItem(item: FbItem) = this.primaryItem(item.titleId) {
iicon = item.icon
iconColor = Prefs.textColor.toLong()
textColor = Prefs.textColor.toLong()
selectedIconColor = Prefs.textColor.toLong()
selectedTextColor = Prefs.textColor.toLong()
selectedColor = 0x00000001.toLong()
identifier = item.titleId.toLong()
onClick { _ ->
frostAnswers {
logContentView(ContentViewEvent()
.putContentName(item.name)
.putContentType("drawer_item"))
}
launchWebOverlay(item.url)
false
}
}
private fun Builder.secondaryFrostItem(@StringRes title: Int, onClick: () -> Unit) = this.secondaryItem(title) {
textColor = Prefs.textColor.toLong()
selectedIconColor = Prefs.textColor.toLong()
selectedTextColor = Prefs.textColor.toLong()
selectedColor = 0x00000001.toLong()
identifier = title.toLong()
onClick { _ -> onClick(); false }
}
fun refreshAll() {
fragmentSubject.onNext(REQUEST_REFRESH)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
toolbar.tint(Prefs.iconColor)
setMenuIcons(menu, Prefs.iconColor,
R.id.action_settings to GoogleMaterial.Icon.gmd_settings,
R.id.action_search to GoogleMaterial.Icon.gmd_search)
searchViewBindIfNull {
bindSearchView(menu, R.id.action_search, Prefs.iconColor) {
textCallback = { query, _ ->
val results = searchViewCache[query]
if (results != null)
runOnUiThread { searchView?.results = results }
else
doAsync {
val data = SearchParser.query(query) ?: return@doAsync
val items = data.map { SearchItem(it.href, it.title, it.description) }.toMutableList()
if (items.isNotEmpty())
items.add(SearchItem("${FbItem._SEARCH.url}?q=$query", string(R.string.show_all_results), iicon = null))
searchViewCache.put(query, items)
uiThread { searchView?.results = items }
}
}
textDebounceInterval = 300
searchCallback = { query, _ -> launchWebOverlay("${FbItem._SEARCH.url}/?q=$query"); true }
closeListener = { _ -> searchViewCache.clear() }
foregroundColor = Prefs.textColor
backgroundColor = Prefs.bgColor.withMinAlpha(200)
onItemClick = { _, key, _, _ -> launchWebOverlay(key) }
}
}
return true
}
@SuppressLint("RestrictedApi")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings -> {
val intent = Intent(this, SettingsActivity::class.java)
intent.putParcelableArrayListExtra(EXTRA_COOKIES, cookies())
val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out).toBundle()
startActivityForResult(intent, ACTIVITY_SETTINGS, bundle)
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun openFileChooser(filePathCallback: ValueCallback<Array<Uri>?>, fileChooserParams: WebChromeClient.FileChooserParams) {
openMediaPicker(filePathCallback, fileChooserParams)
}
@SuppressLint("NewApi")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (onActivityResultWeb(requestCode, resultCode, data)) return
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ACTIVITY_SETTINGS) {
if (resultCode and REQUEST_RESTART_APPLICATION > 0) { //completely restart application
L.d("Restart Application Requested")
val intent = packageManager.getLaunchIntentForPackage(packageName)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
val pending = PendingIntent.getActivity(this, 666, intent, PendingIntent.FLAG_CANCEL_CURRENT)
val alarm = getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (buildIsMarshmallowAndUp)
alarm.setExactAndAllowWhileIdle(AlarmManager.RTC, System.currentTimeMillis() + 100, pending)
else
alarm.setExact(AlarmManager.RTC, System.currentTimeMillis() + 100, pending)
finish()
System.exit(0)
return
}
if (resultCode and REQUEST_RESTART > 0) return restart()
/*
* These results can be stacked
*/
if (resultCode and REQUEST_REFRESH > 0) fragmentSubject.onNext(REQUEST_REFRESH)
if (resultCode and REQUEST_NAV > 0) frostNavigationBar()
if (resultCode and REQUEST_TEXT_ZOOM > 0) fragmentSubject.onNext(REQUEST_TEXT_ZOOM)
if (resultCode and REQUEST_SEARCH > 0) invalidateOptionsMenu()
}
}
override fun onResume() {
super.onResume()
FbCookie.switchBackUser { }
}
override fun onStart() {
//validate some pro features
if (!IS_FROST_PRO) {
if (Prefs.theme == Theme.CUSTOM.ordinal) Prefs.theme = Theme.DEFAULT.ordinal
}
super.onStart()
}
override fun onDestroy() {
onDestroyBilling()
super.onDestroy()
}
override fun backConsumer(): Boolean {
if (currentFragment.onBackPressed()) return true
if (Prefs.exitConfirmation) {
materialDialogThemed {
title(R.string.kau_exit)
content(R.string.kau_exit_confirmation)
positiveText(R.string.kau_yes)
negativeText(R.string.kau_no)
onPositive { _, _ -> finish() }
checkBoxPromptRes(R.string.kau_do_not_show_again, false, { _, b -> Prefs.exitConfirmation = !b })
}
return true
}
return false
}
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 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
}
override fun getCount() = pages.size
override fun getPageTitle(position: Int): CharSequence = getString(pages[position].titleId)
}
override val lowerVideoPadding: PointF
get() =
if (Prefs.mainActivityLayout == MainActivityLayout.BOTTOM_BAR)
PointF(0f, toolbar.height.toFloat())
else
PointF(0f, 0f)
}

View File

@ -1,98 +1,20 @@
package com.pitchedapps.frost.activities
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.PointF
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.support.annotation.StringRes
import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.TabLayout
import android.support.v4.app.ActivityOptionsCompat
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentPagerAdapter
import android.support.v4.view.ViewPager
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.widget.FrameLayout
import ca.allanwang.kau.searchview.SearchItem
import ca.allanwang.kau.searchview.SearchView
import ca.allanwang.kau.searchview.SearchViewHolder
import ca.allanwang.kau.searchview.bindSearchView
import ca.allanwang.kau.utils.*
import co.zsmb.materialdrawerkt.builders.Builder
import co.zsmb.materialdrawerkt.builders.accountHeader
import co.zsmb.materialdrawerkt.builders.drawer
import co.zsmb.materialdrawerkt.draweritems.badgeable.primaryItem
import co.zsmb.materialdrawerkt.draweritems.badgeable.secondaryItem
import co.zsmb.materialdrawerkt.draweritems.divider
import co.zsmb.materialdrawerkt.draweritems.profile.profile
import co.zsmb.materialdrawerkt.draweritems.profile.profileSetting
import com.crashlytics.android.answers.ContentViewEvent
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.Drawer
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.ActivityWebContract
import com.pitchedapps.frost.contracts.FileChooserContract
import com.pitchedapps.frost.contracts.FileChooserDelegate
import com.pitchedapps.frost.contracts.VideoViewHolder
import com.pitchedapps.frost.dbflow.TAB_COUNT
import com.pitchedapps.frost.dbflow.loadFbCookie
import com.pitchedapps.frost.dbflow.loadFbTabs
import com.pitchedapps.frost.enums.MainActivityLayout
import com.pitchedapps.frost.enums.Theme
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbCookie.switchUser
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.parsers.SearchParser
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.iab.FrostBilling
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import com.pitchedapps.frost.utils.iab.IabMain
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.views.BadgedIcon
import com.pitchedapps.frost.views.FrostVideoViewer
import com.pitchedapps.frost.views.FrostViewPager
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
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
class MainActivity : BaseActivity(),
ActivityWebContract, FileChooserContract by FileChooserDelegate(),
VideoViewHolder, SearchViewHolder,
FrostBilling by IabMain() {
class MainActivity : BaseMainActivity() {
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)
val fab: FloatingActionButton by bindView(R.id.fab)
val tabs: TabLayout by bindView(R.id.tabs)
val appBar: AppBarLayout by bindView(R.id.appbar)
val coordinator: CoordinatorLayout by bindView(R.id.main_content)
override var videoViewer: FrostVideoViewer? = null
lateinit var drawer: Drawer
lateinit var drawerHeader: AccountHeader
var webFragmentObservable = PublishSubject.create<Int>()!!
override val fragmentSubject = PublishSubject.create<Int>()!!
var lastPosition = -1
val headerBadgeObservable = PublishSubject.create<String>()
var firstLoadFinished = false
@ -101,47 +23,20 @@ class MainActivity : BaseActivity(),
L.i("First fragment load has finished")
field = value
}
override var searchView: SearchView? = null
private val searchViewCache = mutableMapOf<String, List<SearchItem>>()
companion object {
const val ACTIVITY_SETTINGS = 97
/*
* Possible responses from the SettingsActivity
* after the configurations have changed
*/
const val REQUEST_RESTART_APPLICATION = 1 shl 1
const val REQUEST_RESTART = 1 shl 2
const val REQUEST_REFRESH = 1 shl 3
const val REQUEST_WEB_ZOOM = 1 shl 4
const val REQUEST_NAV = 1 shl 5
const val REQUEST_SEARCH = 1 shl 6
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (BuildConfig.VERSION_CODE > Prefs.versionCode) {
Prefs.versionCode = BuildConfig.VERSION_CODE
if (!BuildConfig.DEBUG) {
frostChangelog()
frostAnswersCustom("Version",
"Version code" to BuildConfig.VERSION_CODE,
"Version name" to BuildConfig.VERSION_NAME,
"Build type" to BuildConfig.BUILD_TYPE,
"Frost id" to Prefs.frostId)
}
}
setFrameContentView(Prefs.mainActivityLayout.layoutRes)
setSupportActionBar(toolbar)
adapter = SectionsPagerAdapter(supportFragmentManager, loadFbTabs())
viewPager.adapter = adapter
viewPager.offscreenPageLimit = TAB_COUNT
setupViewPager()
setupTabs()
}
private fun setupViewPager() {
viewPager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (lastPosition == position) return
if (lastPosition != -1) webFragmentObservable.onNext(-(lastPosition + 1))
webFragmentObservable.onNext(position)
if (lastPosition != -1) fragmentSubject.onNext(-(lastPosition + 1))
fragmentSubject.onNext(position)
lastPosition = position
}
@ -157,30 +52,17 @@ class MainActivity : BaseActivity(),
}
}
})
viewPager.post { webFragmentObservable.onNext(0); lastPosition = 0 } //trigger hook so title is set
setupDrawer(savedInstanceState)
setupTabs()
// fab.setOnClickListener { view ->
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
// .setAction("Action", null).show()
// }
setFrostColors(toolbar, themeWindow = false, headers = arrayOf(appBar), backgrounds = arrayOf(viewPager))
tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor())
onCreateBilling()
viewPager.post { fragmentSubject.onNext(0); lastPosition = 0 } //trigger hook so title is set
}
fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) {
(0 until tabs.tabCount).asSequence().forEach { i ->
action(i, tabs.getTabAt(i)!!.customView as BadgedIcon)
}
}
fun setupTabs() {
private fun setupTabs() {
viewPager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
tabs.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(viewPager) {
override fun onTabReselected(tab: TabLayout.Tab) {
super.onTabReselected(tab)
currentFragment.web.scrollOrRefresh()
currentFragment.onTabClick()
}
override fun onTabSelected(tab: TabLayout.Tab) {
@ -215,274 +97,4 @@ class MainActivity : BaseActivity(),
}
}
fun setupDrawer(savedInstanceState: Bundle?) {
val navBg = Prefs.bgColor.withMinAlpha(200).toLong()
val navHeader = Prefs.headerColor.withMinAlpha(200)
drawer = drawer {
toolbar = this@MainActivity.toolbar
savedInstance = savedInstanceState
translucentStatusBar = false
sliderBackgroundColor = navBg
drawerHeader = accountHeader {
customViewRes = R.layout.material_drawer_header
textColor = Prefs.iconColor.toLong()
backgroundDrawable = ColorDrawable(navHeader)
selectionSecondLineShown = false
cookies().forEach { (id, name) ->
profile(name = name ?: "") {
iconUrl = PROFILE_PICTURE_URL(id)
textColor = Prefs.textColor.toLong()
selectedTextColor = Prefs.textColor.toLong()
selectedColor = 0x00000001.toLong()
identifier = id
}
}
profileSetting(nameRes = R.string.kau_logout) {
iicon = GoogleMaterial.Icon.gmd_exit_to_app
iconColor = Prefs.textColor.toLong()
textColor = Prefs.textColor.toLong()
identifier = -2L
}
profileSetting(nameRes = R.string.kau_add_account) {
iconDrawable = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_add).actionBar().paddingDp(5).color(Prefs.textColor)
textColor = Prefs.textColor.toLong()
identifier = -3L
}
profileSetting(nameRes = R.string.kau_manage_account) {
iicon = GoogleMaterial.Icon.gmd_settings
iconColor = Prefs.textColor.toLong()
textColor = Prefs.textColor.toLong()
identifier = -4L
}
onProfileChanged { _, profile, current ->
if (current) launchWebOverlay(FbItem.PROFILE.url)
else when (profile.identifier) {
-2L -> {
val currentCookie = loadFbCookie(Prefs.userId)
if (currentCookie == null) {
toast(R.string.account_not_found)
FbCookie.reset { launchLogin(cookies(), true) }
} else {
materialDialogThemed {
title(R.string.kau_logout)
content(String.format(string(R.string.kau_logout_confirm_as_x), currentCookie.name ?: Prefs.userId.toString()))
positiveText(R.string.kau_yes)
negativeText(R.string.kau_no)
onPositive { _, _ -> FbCookie.logout(this@MainActivity) }
}
}
}
-3L -> launchNewTask(LoginActivity::class.java, clearStack = false)
-4L -> launchNewTask(SelectorActivity::class.java, cookies(), false)
else -> {
switchUser(profile.identifier, { refreshAll() })
tabsForEachView { _, view -> view.badgeText = null }
}
}
false
}
}
drawerHeader.setActiveProfile(Prefs.userId)
primaryFrostItem(FbItem.FEED_MOST_RECENT)
primaryFrostItem(FbItem.FEED_TOP_STORIES)
primaryFrostItem(FbItem.ACTIVITY_LOG)
divider()
primaryFrostItem(FbItem.PHOTOS)
primaryFrostItem(FbItem.GROUPS)
primaryFrostItem(FbItem.FRIENDS)
primaryFrostItem(FbItem.CHAT)
primaryFrostItem(FbItem.PAGES)
divider()
primaryFrostItem(FbItem.EVENTS)
primaryFrostItem(FbItem.BIRTHDAYS)
primaryFrostItem(FbItem.ON_THIS_DAY)
divider()
primaryFrostItem(FbItem.NOTES)
primaryFrostItem(FbItem.SAVED)
}
}
private fun Builder.primaryFrostItem(item: FbItem) = this.primaryItem(item.titleId) {
iicon = item.icon
iconColor = Prefs.textColor.toLong()
textColor = Prefs.textColor.toLong()
selectedIconColor = Prefs.textColor.toLong()
selectedTextColor = Prefs.textColor.toLong()
selectedColor = 0x00000001.toLong()
identifier = item.titleId.toLong()
onClick { _ ->
frostAnswers {
logContentView(ContentViewEvent()
.putContentName(item.name)
.putContentType("drawer_item"))
}
launchWebOverlay(item.url)
false
}
}
private fun Builder.secondaryFrostItem(@StringRes title: Int, onClick: () -> Unit) = this.secondaryItem(title) {
textColor = Prefs.textColor.toLong()
selectedIconColor = Prefs.textColor.toLong()
selectedTextColor = Prefs.textColor.toLong()
selectedColor = 0x00000001.toLong()
identifier = title.toLong()
onClick { _ -> onClick(); false }
}
fun refreshAll() {
webFragmentObservable.onNext(WebFragment.REQUEST_REFRESH)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
toolbar.tint(Prefs.iconColor)
setMenuIcons(menu, Prefs.iconColor,
R.id.action_settings to GoogleMaterial.Icon.gmd_settings,
R.id.action_search to GoogleMaterial.Icon.gmd_search)
searchViewBindIfNull {
bindSearchView(menu, R.id.action_search, Prefs.iconColor) {
textCallback = { query, _ ->
val results = searchViewCache[query]
if (results != null)
runOnUiThread { searchView?.results = results }
else
doAsync {
val data = SearchParser.query(query) ?: return@doAsync
val items = data.map { SearchItem(it.href, it.title, it.description) }.toMutableList()
if (items.isNotEmpty())
items.add(SearchItem("${FbItem._SEARCH.url}?q=$query", string(R.string.show_all_results), iicon = null))
searchViewCache.put(query, items)
uiThread { searchView?.results = items }
}
}
textDebounceInterval = 300
searchCallback = { query, _ -> launchWebOverlay("${FbItem._SEARCH.url}/?q=$query"); true }
closeListener = { _ -> searchViewCache.clear() }
foregroundColor = Prefs.textColor
backgroundColor = Prefs.bgColor.withMinAlpha(200)
onItemClick = { _, key, _, _ -> launchWebOverlay(key) }
}
}
return true
}
@SuppressLint("RestrictedApi")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings -> {
val intent = Intent(this, SettingsActivity::class.java)
intent.putParcelableArrayListExtra(EXTRA_COOKIES, cookies())
val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out).toBundle()
startActivityForResult(intent, ACTIVITY_SETTINGS, bundle)
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun openFileChooser(filePathCallback: ValueCallback<Array<Uri>?>, fileChooserParams: WebChromeClient.FileChooserParams) {
openMediaPicker(filePathCallback, fileChooserParams)
}
@SuppressLint("NewApi")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (onActivityResultWeb(requestCode, resultCode, data)) return
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ACTIVITY_SETTINGS) {
if (resultCode and REQUEST_RESTART_APPLICATION > 0) { //completely restart application
L.d("Restart Application Requested")
val intent = packageManager.getLaunchIntentForPackage(packageName)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
val pending = PendingIntent.getActivity(this, 666, intent, PendingIntent.FLAG_CANCEL_CURRENT)
val alarm = getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (buildIsMarshmallowAndUp)
alarm.setExactAndAllowWhileIdle(AlarmManager.RTC, System.currentTimeMillis() + 100, pending)
else
alarm.setExact(AlarmManager.RTC, System.currentTimeMillis() + 100, pending)
finish()
System.exit(0)
return
}
if (resultCode and REQUEST_RESTART > 0) return restart()
/*
* These results can be stacked
*/
if (resultCode and REQUEST_REFRESH > 0) webFragmentObservable.onNext(WebFragment.REQUEST_REFRESH)
if (resultCode and REQUEST_NAV > 0) frostNavigationBar()
if (resultCode and REQUEST_WEB_ZOOM > 0) webFragmentObservable.onNext(WebFragment.REQUEST_TEXT_ZOOM)
if (resultCode and REQUEST_SEARCH > 0) invalidateOptionsMenu()
}
}
override fun onResume() {
super.onResume()
FbCookie.switchBackUser { }
}
override fun onStart() {
//validate some pro features
if (!IS_FROST_PRO) {
if (Prefs.theme == Theme.CUSTOM.ordinal) Prefs.theme = Theme.DEFAULT.ordinal
}
super.onStart()
}
override fun onDestroy() {
onDestroyBilling()
super.onDestroy()
}
override fun backConsumer(): Boolean {
if (currentFragment.onBackPressed()) return true
if (Prefs.exitConfirmation) {
materialDialogThemed {
title(R.string.kau_exit)
content(R.string.kau_exit_confirmation)
positiveText(R.string.kau_yes)
negativeText(R.string.kau_no)
onPositive { _, _ -> finish() }
checkBoxPromptRes(R.string.kau_do_not_show_again, false, { _, b -> Prefs.exitConfirmation = !b })
}
return true
}
return false
}
inline val currentFragment
get() = supportFragmentManager.findFragmentByTag("android:switcher:${R.id.container}:${viewPager.currentItem}") as WebFragment
inner class SectionsPagerAdapter(fm: FragmentManager, val pages: List<FbItem>) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
val fragment = WebFragment(pages[position], position)
//If first load hasn't occurred, add a listener
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
}
override fun getCount() = pages.size
override fun getPageTitle(position: Int): CharSequence = getString(pages[position].titleId)
}
override val lowerVideoPadding: PointF
get() =
if (Prefs.mainActivityLayout == MainActivityLayout.BOTTOM_BAR)
PointF(0f, toolbar.height.toFloat())
else
PointF(0f, 0f)
}

View File

@ -161,7 +161,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IabSettings() {
}
fun shouldRestartMain() {
setFrostResult(MainActivity.REQUEST_RESTART)
setFrostResult(REQUEST_RESTART)
}
@SuppressLint("MissingSuperCall")

View File

@ -1,5 +1,6 @@
package com.pitchedapps.frost.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.PointF
import android.net.Uri
@ -18,15 +19,14 @@ import ca.allanwang.kau.utils.*
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.ActivityWebContract
import com.pitchedapps.frost.contracts.FileChooserContract
import com.pitchedapps.frost.contracts.FileChooserDelegate
import com.pitchedapps.frost.contracts.VideoViewHolder
import com.pitchedapps.frost.contracts.*
import com.pitchedapps.frost.enums.OverlayContext
import com.pitchedapps.frost.facebook.*
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.views.FrostContentWeb
import com.pitchedapps.frost.views.FrostVideoViewer
import com.pitchedapps.frost.web.FrostWebView
import com.pitchedapps.frost.views.FrostWebView
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import okhttp3.HttpUrl
@ -56,7 +56,7 @@ class FrostWebActivity : WebOverlayActivityBase(false) {
* and pop a dialog giving the user the option to copy the shared text
*/
var disposable: Disposable? = null
disposable = frostWeb.web.refreshObservable.subscribe {
disposable = content.refreshObservable.subscribe {
disposable?.dispose()
materialDialogThemed {
title(R.string.invalid_share_url)
@ -98,26 +98,36 @@ class WebOverlayBasicActivity : WebOverlayActivityBase(true)
*/
class WebOverlayActivity : WebOverlayActivityBase(false)
@SuppressLint("Registered")
open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseActivity(),
ActivityWebContract, VideoViewHolder, FileChooserContract by FileChooserDelegate() {
ActivityContract, FrostContentContainer,
VideoViewHolder, FileChooserContract by FileChooserDelegate() {
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
val toolbar: Toolbar by bindView(R.id.overlay_toolbar)
val frostWeb: FrostWebView by bindView(R.id.overlay_frost_webview)
val content: FrostContentWeb by bindView(R.id.frost_content_web)
val web: FrostWebView
get() = content.coreView
val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content)
inline val urlTest: String?
private inline val urlTest: String?
get() = intent.extras?.getString(ARG_URL) ?: intent.dataString
open val url: String
override val baseUrl: String
get() = (intent.extras?.getString(ARG_URL) ?: intent.dataString).formattedFbUrl
inline val userId: Long
override val baseEnum: FbItem? = null
private inline val userId: Long
get() = intent.extras?.getLong(ARG_USER_ID, Prefs.userId) ?: Prefs.userId
inline val overlayContext: OverlayContext?
private inline val overlayContext: OverlayContext?
get() = intent.extras?.getSerializable(ARG_OVERLAY_CONTEXT) as OverlayContext?
override fun setTitle(title: String) {
toolbar.title = title
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (urlTest == null) {
@ -136,17 +146,24 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
setFrostColors(toolbar, themeWindow = false)
coordinator.setBackgroundColor(Prefs.bgColor.withAlpha(255))
frostWeb.setupWebview(url)
if (forceBasicAgent)
frostWeb.web.userAgentString = USER_AGENT_BASIC
frostWeb.web.addTitleListener({ toolbar.title = it })
Prefs.prevId = Prefs.userId
if (userId != Prefs.userId) FbCookie.switchUser(userId) { frostWeb.web.loadBaseUrl() }
else frostWeb.web.loadBaseUrl()
if (Showcase.firstWebOverlay) {
coordinator.frostSnackbar(R.string.web_overlay_swipe_hint) {
duration = Snackbar.LENGTH_INDEFINITE
setAction(R.string.kau_got_it) { _ -> this.dismiss() }
content.bind(this)
web.reloadBase(true)
content.titleObservable
.observeOn(AndroidSchedulers.mainThread())
.subscribe { toolbar.title = it }
with(web) {
if (forceBasicAgent)
userAgentString = USER_AGENT_BASIC
Prefs.prevId = Prefs.userId
if (userId != Prefs.userId) FbCookie.switchUser(userId) { reloadBase(true) }
else reloadBase(true)
if (Showcase.firstWebOverlay) {
coordinator.frostSnackbar(R.string.web_overlay_swipe_hint) {
duration = Snackbar.LENGTH_INDEFINITE
setAction(R.string.kau_got_it) { _ -> this.dismiss() }
}
}
}
@ -165,15 +182,15 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
super.onNewIntent(intent)
val newUrl = (intent.extras?.getString(ARG_URL) ?: intent.dataString ?: return).formattedFbUrl
L.d("New intent")
if (url != newUrl) {
if (baseUrl != newUrl) {
this.intent = intent
frostWeb.web.baseUrl = newUrl
frostWeb.web.loadBaseUrl()
content.baseUrl = newUrl
web.reloadBase(true)
}
}
override fun backConsumer(): Boolean {
if (!frostWeb.onBackPressed())
if (!web.onBackPressed())
finishSlideOut()
return true
}
@ -216,9 +233,9 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_copy_link -> copyToClipboard(frostWeb.web.url)
R.id.action_share -> shareText(frostWeb.web.url)
else -> if (!OverlayContext.onOptionsItemSelected(frostWeb.web, item.itemId))
R.id.action_copy_link -> copyToClipboard(web.currentUrl)
R.id.action_share -> shareText(web.currentUrl)
else -> if (!OverlayContext.onOptionsItemSelected(web, item.itemId))
return super.onOptionsItemSelected(item)
}
return true

View File

@ -0,0 +1,14 @@
package com.pitchedapps.frost.contracts
import io.reactivex.subjects.PublishSubject
/**
* All the contracts for [MainActivity]
*/
interface ActivityContract : FileChooserActivityContract
interface MainActivityContract : ActivityContract {
val fragmentSubject: PublishSubject<Int>
fun setTitle(res: Int)
fun setTitle(text: CharSequence)
}

View File

@ -0,0 +1,30 @@
package com.pitchedapps.frost.contracts
/**
* Functions that will modify the current ui
*/
interface DynamicUiContract {
/**
* Change all necessary view components to the new theme
* Also propagate where applicable
*/
fun reloadTheme()
/**
* Change theme without propagation
*/
fun reloadThemeSelf()
/**
* Change text size & propagate
*/
fun reloadTextSize()
/**
* Change text size without propagation
*/
fun reloadTextSizeSelf()
}

View File

@ -0,0 +1,140 @@
package com.pitchedapps.frost.contracts
import android.view.View
import com.pitchedapps.frost.facebook.FbItem
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject
/**
* Created by Allan Wang on 20/12/17.
*/
/**
* Contract for the underlying parent,
* binds to activities & fragments
*/
interface FrostContentContainer {
val baseUrl: String
val baseEnum: FbItem?
/**
* Update toolbar title
*/
fun setTitle(title: String)
}
/**
* Contract for components shared among
* all content providers
*/
interface FrostContentParent : DynamicUiContract {
val core: FrostContentCore
/**
* Observable to get data on whether view is refreshing or not
*/
val refreshObservable: PublishSubject<Boolean>
/**
* Observable to get data on refresh progress, with range [0, 100]
*/
val progressObservable: PublishSubject<Int>
/**
* Observable to get new title data (unique values only)
*/
val titleObservable: BehaviorSubject<String>
var baseUrl: String
var baseEnum: FbItem?
/**
* Binds the container to self
* this will also handle all future bindings
* Must be called by container!
*/
fun bind(container: FrostContentContainer)
/**
* Signal that the contract will not be used again
* Clean up resources where applicable
*/
fun destroy()
/**
* Hook onto the refresh observable for one cycle
* Animate toggles between the fancy ripple and the basic fade
* The cycle only starts on the first load since
* there may have been another process when this is registered
*/
fun registerTransition(animate: Boolean)
}
/**
* Underlying contract for the content itself
*/
interface FrostContentCore : DynamicUiContract {
/**
* Reference to parent
* Bound through calling [FrostContentParent.bind]
*/
var parent: FrostContentParent
/**
* Initializes view through given [container]
*
* The content may be free to extract other data from
* the container if necessary
*
* [parent] must be bounded before calling this!
*/
fun bind(container: FrostContentContainer): View
/**
* Call to reload wrapped data
*/
fun reload(animate: Boolean)
/**
* Call to reload base data
*/
fun reloadBase(animate: Boolean)
/**
* If possible, remove anything in the view stack
* Applies namely to webviews
*/
fun clearHistory()
/**
* Should be called when a back press is triggered
* Return [true] if consumed, [false] otherwise
*/
fun onBackPressed(): Boolean
val currentUrl: String
/**
* Condition to help pause certain background resources
*/
var active: Boolean
/**
* Triggered when view is within viewpager
* and tab is clicked
*/
fun onTabClicked()
/**
* Signal destruction to release some content manually
*/
fun destroy()
}

View File

@ -0,0 +1,31 @@
package com.pitchedapps.frost.contracts
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject
/**
* Created by Allan Wang on 2017-11-07.
*/
interface FrostObservables {
/**
* Observable to get data on whether view is refreshing or not
*/
var refreshObservable: PublishSubject<Boolean>
/**
* Observable to get data on refresh progress, with range [0, 100]
*/
var progressObservable: PublishSubject<Int>
/**
* Observable to get new title data (unique values only)
*/
var titleObservable: BehaviorSubject<String>
fun passObservablesTo(other: FrostObservables) {
other.refreshObservable = refreshObservable
other.progressObservable = progressObservable
other.titleObservable = titleObservable
}
}

View File

@ -0,0 +1,29 @@
package com.pitchedapps.frost.contracts
import android.view.View
import android.widget.TextView
/**
* Created by Allan Wang on 2017-11-07.
*
* Should be implemented by all views in [com.pitchedapps.frost.activities.MainActivity]
* to allow for instant view reloading
*/
interface FrostThemable {
/**
* Change all necessary view components to the new theme
* and call whatever other children that also implement [FrostThemable]
*/
fun reloadTheme()
fun setTextColors(color: Int, vararg textViews: TextView?) =
themeViews(color, *textViews) { setTextColor(it) }
fun setBackgrounds(color: Int, vararg views: View?) =
themeViews(color, *views) { setBackgroundColor(it) }
fun <T : View> themeViews(color: Int, vararg views: T?, action: T.(Int) -> Unit) =
views.filterNotNull().forEach { it.action(color) }
}

View File

@ -0,0 +1,25 @@
package com.pitchedapps.frost.contracts
import com.pitchedapps.frost.facebook.FbItem
/**
* Created by Allan Wang on 19/12/17.
*/
interface FrostUrlData {
/**
* The main (and fallback) url
*/
var baseUrl: String
/**
* Only base viewpager should pass an enum
*/
var baseEnum: FbItem?
fun passUrlDataTo(other: FrostUrlData) {
other.baseUrl = baseUrl
other.baseEnum = baseEnum
}
}

View File

@ -1,8 +0,0 @@
package com.pitchedapps.frost.contracts
/**
* Created by Allan Wang on 2017-07-04.
*
* Combination of all the core functions implemented by the Activity
*/
interface ActivityWebContract : FileChooserActivityContract

View File

@ -4,10 +4,9 @@ import android.content.Context
import android.view.Menu
import android.view.MenuItem
import ca.allanwang.kau.utils.toDrawable
import com.mikepenz.iconics.typeface.IIcon
import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.web.FrostWebViewCore
import com.pitchedapps.frost.views.FrostWebView
/**
* Created by Allan Wang on 2017-09-16.
@ -19,12 +18,8 @@ import com.pitchedapps.frost.web.FrostWebViewCore
*/
enum class OverlayContext(private val menuItem: FrostMenuItem?) {
NOTIFICATION(FrostMenuItem(R.id.action_notification, FbItem.NOTIFICATIONS.icon, R.string.notifications) { webview ->
webview.loadUrl(FbItem.NOTIFICATIONS.url, true)
}),
MESSAGE(FrostMenuItem(R.id.action_messages, FbItem.MESSAGES.icon, R.string.messages) { webview ->
webview.loadUrl(FbItem.MESSAGES.url, true)
});
NOTIFICATION(FrostMenuItem(R.id.action_notification, FbItem.NOTIFICATIONS)),
MESSAGE(FrostMenuItem(R.id.action_messages, FbItem.MESSAGES));
/**
* Inject the [menuItem] in the order that they are given at the front of the menu
@ -40,9 +35,9 @@ enum class OverlayContext(private val menuItem: FrostMenuItem?) {
* Execute selection call for an item by id
* Returns [true] if selection was consumed, [false] otherwise
*/
fun onOptionsItemSelected(webview: FrostWebViewCore, id: Int): Boolean {
val consumer = values.firstOrNull { id == it.menuItem?.id } ?: return false
consumer.menuItem!!.onClick(webview)
fun onOptionsItemSelected(web: FrostWebView, id: Int): Boolean {
val item = values.firstOrNull { id == it.menuItem?.id }?.menuItem ?: return false
web.loadUrl(item.fbItem.url, true)
return true
}
}
@ -53,13 +48,11 @@ enum class OverlayContext(private val menuItem: FrostMenuItem?) {
*/
class FrostMenuItem(
val id: Int,
val iicon: IIcon,
val stringRes: Int,
val showAsAction: Int = MenuItem.SHOW_AS_ACTION_ALWAYS,
val onClick: (webview: FrostWebViewCore) -> Unit) {
val fbItem: FbItem,
val showAsAction: Int = MenuItem.SHOW_AS_ACTION_ALWAYS) {
fun addToMenu(context: Context, menu: Menu, index: Int) {
val item = menu.add(Menu.NONE, id, index, stringRes)
item.icon = iicon.toDrawable(context, 18)
val item = menu.add(Menu.NONE, id, index, fbItem.titleId)
item.icon = fbItem.icon.toDrawable(context, 18)
item.setShowAsAction(showAsAction)
}
}

View File

@ -6,15 +6,15 @@ 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.web.FrostWebViewClient
import com.pitchedapps.frost.web.FrostWebViewClientMenu
import com.pitchedapps.frost.web.FrostWebViewCore
import com.pitchedapps.frost.fragments.BaseFragment
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.fragments.WebFragmentMenu
enum class FbItem(
@StringRes val titleId: Int,
val icon: IIcon,
relativeUrl: String,
val webClient: ((webCore: FrostWebViewCore) -> FrostWebViewClient)? = null
val fragmentCreator: () -> BaseFragment = ::WebFragment
) {
ACTIVITY_LOG(R.string.activity_log, GoogleMaterial.Icon.gmd_list, "me/allactivity"),
BIRTHDAYS(R.string.birthdays, GoogleMaterial.Icon.gmd_cake, "events/birthdays"),
@ -25,7 +25,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", { FrostWebViewClientMenu(it) }),
MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "settings", ::WebFragmentMenu),
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"),

View File

@ -0,0 +1,234 @@
package com.pitchedapps.frost.fragments
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import ca.allanwang.kau.utils.withArguments
import com.mikepenz.fastadapter.IItem
import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.DynamicUiContract
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.contracts.MainActivityContract
import com.pitchedapps.frost.enums.FeedSort
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.parsers.FrostParser
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.views.FrostRecyclerView
import com.pitchedapps.frost.views.FrostWebView
import com.pitchedapps.frost.web.FrostWebViewClient
import com.pitchedapps.frost.web.FrostWebViewClientMenu
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
/**
* Created by Allan Wang on 2017-11-07.
*/
abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
companion object {
private const val ARG_URL_ENUM = "arg_url_enum"
private const val ARG_POSITION = "arg_position"
internal operator fun invoke(base: () -> BaseFragment, data: FbItem, position: Int): BaseFragment {
val fragment = if (Prefs.nativeViews) base() else WebFragment()
val d = if (data == FbItem.FEED) FeedSort(Prefs.feedSort).item else data
fragment.withArguments(
ARG_URL to d.url,
ARG_POSITION to position,
ARG_URL_ENUM to d
)
return fragment
}
}
override val baseUrl: String by lazy { arguments!!.getString(ARG_URL) }
override val baseEnum: FbItem by lazy { arguments!!.getSerializable(ARG_URL_ENUM) as FbItem }
override val position: Int by lazy { arguments!!.getInt(ARG_POSITION) }
override var firstLoad: Boolean = true
private var activityDisposable: Disposable? = null
private var onCreateRunnable: ((FragmentContract) -> Unit)? = null
override var content: FrostContentParent? = null
protected abstract val layoutRes: Int
override final fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(layoutRes, container, false)
val content = view as? FrostContentParent
?: throw IllegalArgumentException("layoutRes for fragment must return view implementing FrostContentParent")
this.content = content
content.bind(this)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onCreateRunnable?.invoke(this)
onCreateRunnable = null
firstLoadRequest()
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
firstLoadRequest()
}
override fun firstLoadRequest() {
if (userVisibleHint && isVisible && firstLoad) {
core?.reloadBase(true)
firstLoad = false
}
}
override fun post(action: (fragment: FragmentContract) -> Unit) {
onCreateRunnable = action
}
override fun setTitle(title: String) {
(context as? MainActivityContract)?.setTitle(title)
}
override fun attachMainObservable(contract: MainActivityContract): Disposable =
contract.fragmentSubject.observeOn(AndroidSchedulers.mainThread()).subscribe {
when (it) {
REQUEST_REFRESH -> {
core?.apply {
reload(true)
clearHistory()
}
}
position -> {
contract.setTitle(baseEnum.titleId)
core?.active = true
}
-(position + 1) -> {
core?.active = false
}
REQUEST_TEXT_ZOOM -> {
reloadTextSize()
}
}
}
override fun detachMainObservable() {
activityDisposable?.dispose()
}
override fun onAttach(context: Context) {
super.onAttach(context)
detachMainObservable()
if (context is MainActivityContract)
activityDisposable = attachMainObservable(context)
}
override fun onDetach() {
detachMainObservable()
super.onDetach()
}
override fun onDestroyView() {
content?.destroy()
content = null
super.onDestroyView()
}
override fun reloadTheme() {
reloadThemeSelf()
content?.reloadTextSize()
}
override fun reloadThemeSelf() {
// intentionally blank
}
override fun reloadTextSize() {
reloadTextSizeSelf()
content?.reloadTextSize()
}
override fun reloadTextSizeSelf() {
// intentionally blank
}
override fun onBackPressed(): Boolean = content?.core?.onBackPressed() ?: false
override fun onTabClick(): Unit = content?.core?.onTabClicked() ?: Unit
}
abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), RecyclerContentContract {
override val layoutRes: Int = R.layout.view_content_recycler
/**
* The parser to make this all happen
*/
abstract val parser: FrostParser<T>
abstract val adapter: FastItemAdapter<Item>
abstract fun toItems(data: T): List<Item>
override fun bind(recyclerView: FrostRecyclerView) {
recyclerView.adapter = this.adapter
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val tail = tailMapper(baseEnum)
if (tail.isNotEmpty()) {
val baseUrl = baseEnum.url
L.d("Adding $tail to $baseUrl for RecyclerFragment")
arguments!!.putString(ARG_URL, "$baseUrl$tail")
}
}
private fun tailMapper(item: FbItem) = when (item) {
FbItem.NOTIFICATIONS, FbItem.MESSAGES -> "/?more"
else -> ""
}
override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
doAsync {
progress(10)
val doc = frostJsoup(baseUrl)
progress(60)
val data = parser.parse(doc)
if (data == null) {
context?.toast(R.string.error_generic)
L.eThrow("RecyclerFragment failed for ${baseEnum.name}")
Prefs.nativeViews = false
return@doAsync callback(false)
}
progress(80)
val items = toItems(data)
progress(97)
adapter.setNewList(items)
}
}
}
open class WebFragment : BaseFragment(), FragmentContract {
override val layoutRes: Int = R.layout.view_content_web
/**
* Given a webview, output a client
*/
open fun client(web: FrostWebView) = FrostWebViewClient(web)
override fun innerView(context: Context) = FrostWebView(context)
}
class WebFragmentMenu : WebFragment() {
override fun client(web: FrostWebView) = FrostWebViewClientMenu(web)
}

View File

@ -0,0 +1,92 @@
package com.pitchedapps.frost.fragments
import android.content.Context
import com.pitchedapps.frost.contracts.FrostContentContainer
import com.pitchedapps.frost.contracts.FrostContentCore
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.contracts.MainActivityContract
import com.pitchedapps.frost.views.FrostRecyclerView
import io.reactivex.disposables.Disposable
/**
* Created by Allan Wang on 2017-11-07.
*/
interface FragmentContract : FrostContentContainer {
val content: FrostContentParent?
/**
* Helper to retrieve the core from [content]
*/
val core: FrostContentCore?
get() = content?.core
/**
* Specifies position in Activity's viewpager
*/
val position: Int
/**
* Specifies whether if current load
* will be fragment's first load
*
* Defaults to true
*/
var firstLoad: Boolean
/**
* Called when the fragment is first visible
* Typically, if [firstLoad] is true,
* the fragment should call [reload] and make [firstLoad] false
*/
fun firstLoadRequest()
/**
* Single callable action to be executed upon creation
* Note that this call is not guaranteed
*/
fun post(action: (fragment: FragmentContract) -> Unit)
/**
* Call whenever a fragment is attached so that it may listen
* to activity emissions
*/
fun attachMainObservable(contract: MainActivityContract): Disposable
/**
* Load custom layout to container
*/
fun innerView(context: Context): FrostContentCore
/**
* Call when fragment is detached so that any existing
* observable is disposed
*/
fun detachMainObservable()
/*
* -----------------------------------------
* Delegates
* -----------------------------------------
*/
fun onBackPressed(): Boolean
fun onTabClick()
}
interface RecyclerContentContract {
fun bind(recyclerView: FrostRecyclerView)
/**
* Completely handle data reloading
* Optional progress emission update
* Callback returns [true] for success, [false] otherwise
*/
fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit)
}

View File

@ -1,133 +0,0 @@
package com.pitchedapps.frost.fragments
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import ca.allanwang.kau.utils.withArguments
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.enums.FeedSort
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.web.FrostWebView
import com.pitchedapps.frost.web.FrostWebViewCore
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
/**
* Created by Allan Wang on 2017-05-29.
*/
class WebFragment : Fragment() {
companion object {
private const val ARG_URL = "arg_url"
private const val ARG_URL_ENUM = "arg_url_enum"
private const val ARG_POSITION = "arg_position"
const val REQUEST_TEXT_ZOOM = 17
const val REQUEST_REFRESH = 99
operator fun invoke(data: FbItem, position: Int) = WebFragment().apply {
val d = if (data == FbItem.FEED) FeedSort(Prefs.feedSort).item else data
withArguments(
ARG_URL to d.url,
ARG_POSITION to position,
ARG_URL_ENUM to d
)
}
}
// val refresh: SwipeRefreshLayout by lazy { frostWebView.refresh }
val web: FrostWebViewCore by lazy { frostWebView.web }
val url: String by lazy { arguments!!.getString(ARG_URL) }
val urlEnum: FbItem by lazy { arguments!!.getSerializable(ARG_URL_ENUM) as FbItem }
val position: Int by lazy { arguments!!.getInt(ARG_POSITION) }
lateinit var frostWebView: FrostWebView
private var firstLoad = true
private var activityDisposable: Disposable? = null
private var onCreateRunnable: ((fragment: WebFragment) -> Unit)? = null
/**
* Hook to run action once fragment is properly created
* This is not saved elsewhere and may not always execute
*/
fun post(action: (fragment: WebFragment) -> Unit) {
onCreateRunnable = action
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
frostWebView = FrostWebView(context!!)
frostWebView.setupWebview(url, urlEnum)
return frostWebView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onCreateRunnable?.invoke(this)
onCreateRunnable = null
firstLoad()
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
firstLoad()
}
fun firstLoad() {
if (userVisibleHint && isVisible && firstLoad) {
web.loadBaseUrl()
firstLoad = false
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
activityDisposable?.dispose()
if (context is MainActivity) {
activityDisposable = context.webFragmentObservable.observeOn(AndroidSchedulers.mainThread()).subscribe {
/**
* Execute actions based on flags
* Flags between -10 and 10 are reserved for viewpager events
*/
when (it) {
REQUEST_REFRESH -> {
web.clearHistory()
web.loadBaseUrl(true)
}
position -> {
context.toolbar.setTitle(urlEnum.titleId)
pauseLoad = false
}
-(position + 1) -> { //we are moving away from this fragment
if (!frostWebView.refresh.isRefreshing) pauseLoad = true
}
REQUEST_TEXT_ZOOM -> frostWebView.web.settings.textZoom = Prefs.webTextScaling
}
}
}
}
override fun onDetach() {
activityDisposable?.dispose()
super.onDetach()
}
override fun onResume() {
super.onResume()
pauseLoad = false
firstLoad()
}
var pauseLoad: Boolean
get() = web.settings.blockNetworkLoads
set(value) {
web.settings.blockNetworkLoads = value
}
fun onBackPressed() = frostWebView.onBackPressed()
}

View File

@ -82,7 +82,7 @@ fun WebView.jsInject(vararg injectors: InjectorContract, callback: ((Array<Strin
(0 until validInjectors.size).forEach { i -> validInjectors[i].inject(this, { observables[i].onSuccess(it) }) }
}
fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract, callback: ((Array<String>) -> Unit) = {}) = webCore.jsInject(*injectors, callback = callback)
fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract, callback: ((Array<String>) -> Unit) = {}) = web.jsInject(*injectors, callback = callback)
/**
* Wrapper class to convert a function into an injector

View File

@ -60,8 +60,7 @@ abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
protected fun defaultViewArray(): Array<Array<out View>> = arrayOf(arrayOf(title), arrayOf(image), arrayOf(desc))
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(layoutRes, container, false)
return view
return inflater.inflate(layoutRes, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -70,9 +69,9 @@ abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
}
override fun onDestroyView() {
super.onDestroyView()
Kotterknife.reset(this)
lazyRegistry.invalidateAll()
super.onDestroyView()
}
fun themeFragment() {

View File

@ -10,6 +10,8 @@ import org.jsoup.nodes.Document
*
* In all cases, parsing will be done from a JSoup document
* Variants accepting strings are also permitted, and they will be converted to documents accordingly
* The return type must be nonnull if no parsing errors occurred, as null signifies a parse error
* If null really must be allowed, use Optionals
*/
interface FrostParser<T> {
/**
@ -37,6 +39,7 @@ interface FrostParser<T> {
}
internal abstract class FrostParserBase<T> : FrostParser<T> {
override final fun parse(text: String?): T? {
text ?: return null
val doc = textToDoc(text) ?: return null

View File

@ -6,7 +6,6 @@ import ca.allanwang.kau.kpref.activity.items.KPrefSeekbar
import ca.allanwang.kau.ui.views.RippleCanvas
import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.enums.MainActivityLayout
import com.pitchedapps.frost.enums.Theme
@ -141,7 +140,7 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
checkbox(R.string.rounded_icons, { Prefs.showRoundedIcons }, {
Prefs.showRoundedIcons = it
setFrostResult(MainActivity.REQUEST_REFRESH)
setFrostResult(REQUEST_REFRESH)
}) {
descRes = R.string.rounded_icons_desc
}
@ -149,7 +148,7 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
checkbox(R.string.tint_nav, { Prefs.tintNavBar }, {
Prefs.tintNavBar = it
frostNavigationBar()
setFrostResult(MainActivity.REQUEST_NAV)
setFrostResult(REQUEST_NAV)
}) {
descRes = R.string.tint_nav_desc
}
@ -157,5 +156,5 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
list.add(KPrefTextSeekbar(
KPrefSeekbar.KPrefSeekbarBuilder(
globalOptions,
R.string.web_text_scaling, { Prefs.webTextScaling }, { Prefs.webTextScaling = it; setFrostResult(MainActivity.REQUEST_WEB_ZOOM) })))
R.string.web_text_scaling, { Prefs.webTextScaling }, { Prefs.webTextScaling = it; setFrostResult(REQUEST_TEXT_ZOOM) })))
}

View File

@ -2,9 +2,9 @@ package com.pitchedapps.frost.settings
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.REQUEST_REFRESH
/**
* Created by Allan Wang on 2017-06-30.
@ -15,7 +15,7 @@ fun SettingsActivity.getBehaviourPrefs(): KPrefAdapterBuilder.() -> Unit = {
descRes = R.string.fancy_animations_desc
}
checkbox(R.string.overlay_swipe, { Prefs.overlayEnabled }, { Prefs.overlayEnabled = it; setFrostResult(MainActivity.REQUEST_REFRESH) }) {
checkbox(R.string.overlay_swipe, { Prefs.overlayEnabled }, { Prefs.overlayEnabled = it; setFrostResult(REQUEST_REFRESH) }) {
descRes = R.string.overlay_swipe_desc
}

View File

@ -3,10 +3,10 @@ package com.pitchedapps.frost.settings
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import ca.allanwang.kau.logging.KL
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.REQUEST_RESTART_APPLICATION
import com.pitchedapps.frost.utils.Showcase
/**
@ -40,7 +40,7 @@ fun SettingsActivity.getExperimentalPrefs(): KPrefAdapterBuilder.() -> Unit = {
plainText(R.string.restart_frost) {
descRes = R.string.restart_frost_desc
onClick = {
setFrostResult(MainActivity.REQUEST_RESTART_APPLICATION)
setFrostResult(REQUEST_RESTART_APPLICATION)
finish()
}
}

View File

@ -3,10 +3,10 @@ package com.pitchedapps.frost.settings
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.enums.FeedSort
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.REQUEST_REFRESH
import com.pitchedapps.frost.utils.materialDialogThemed
/**
@ -34,14 +34,14 @@ fun SettingsActivity.getFeedPrefs(): KPrefAdapterBuilder.() -> Unit = {
checkbox(R.string.aggressive_recents, { Prefs.aggressiveRecents }, {
Prefs.aggressiveRecents = it
setFrostResult(MainActivity.REQUEST_REFRESH)
setFrostResult(REQUEST_REFRESH)
}) {
descRes = R.string.aggressive_recents_desc
}
checkbox(R.string.composer, { Prefs.showComposer }, {
Prefs.showComposer = it
setFrostResult(MainActivity.REQUEST_REFRESH)
setFrostResult(REQUEST_REFRESH)
}) {
descRes = R.string.composer_desc
}
@ -50,7 +50,7 @@ fun SettingsActivity.getFeedPrefs(): KPrefAdapterBuilder.() -> Unit = {
checkbox(R.string.suggested_friends, { Prefs.showSuggestedFriends }, {
Prefs.showSuggestedFriends = it
setFrostResult(MainActivity.REQUEST_REFRESH)
setFrostResult(REQUEST_REFRESH)
}) {
descRes = R.string.suggested_friends_desc
dependsOnPro()
@ -58,7 +58,7 @@ fun SettingsActivity.getFeedPrefs(): KPrefAdapterBuilder.() -> Unit = {
checkbox(R.string.suggested_groups, { Prefs.showSuggestedGroups }, {
Prefs.showSuggestedGroups = it
setFrostResult(MainActivity.REQUEST_REFRESH)
setFrostResult(REQUEST_REFRESH)
}) {
descRes = R.string.suggested_groups_desc
dependsOnPro()
@ -66,7 +66,7 @@ fun SettingsActivity.getFeedPrefs(): KPrefAdapterBuilder.() -> Unit = {
checkbox(R.string.facebook_ads, { Prefs.showFacebookAds }, {
Prefs.showFacebookAds = it
setFrostResult(MainActivity.REQUEST_REFRESH)
setFrostResult(REQUEST_REFRESH)
}) {
descRes = R.string.facebook_ads_desc
dependsOnPro()

View File

@ -0,0 +1,16 @@
package com.pitchedapps.frost.utils
/**
* Created by Allan Wang on 20/12/17.
*/
const val ACTIVITY_SETTINGS = 97
/*
* Possible responses from the SettingsActivity
* after the configurations have changed
*/
const val REQUEST_RESTART_APPLICATION = 1 shl 11
const val REQUEST_RESTART = 1 shl 12
const val REQUEST_REFRESH = 1 shl 13
const val REQUEST_TEXT_ZOOM = 1 shl 14
const val REQUEST_NAV = 1 shl 15
const val REQUEST_SEARCH = 1 shl 16

View File

@ -151,5 +151,7 @@ object Prefs : KPref() {
val mainActivityLayout: MainActivityLayout
get() = MainActivityLayout(mainActivityLayoutType)
var nativeViews: Boolean by kpref("native_views", true)
override fun deleteKeys() = arrayOf("search_bar")
}

View File

@ -7,12 +7,8 @@ import ca.allanwang.kau.utils.startPlayStoreLink
import ca.allanwang.kau.utils.string
import com.crashlytics.android.answers.PurchaseEvent
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostAnswers
import com.pitchedapps.frost.utils.materialDialogThemed
import com.pitchedapps.frost.utils.*
/**
* Created by Allan Wang on 2017-06-30.
@ -27,7 +23,7 @@ private fun playStoreLog(text: String) {
*/
private fun Activity.playRestart() {
if (this is SettingsActivity) {
setResult(MainActivity.REQUEST_RESTART)
setResult(REQUEST_RESTART)
finish()
} else restart()
}

View File

@ -25,7 +25,7 @@ import com.pitchedapps.frost.utils.withRoundIcon
class AccountItem(val cookie: CookieModel?) : KauIItem<AccountItem, AccountItem.ViewHolder>
(R.layout.view_account, { ViewHolder(it) }, R.id.item_account) {
override fun bindView(viewHolder: ViewHolder, payloads: List<Any>?) {
override fun bindView(viewHolder: ViewHolder, payloads: MutableList<Any>) {
super.bindView(viewHolder, payloads)
with(viewHolder) {
text.invisible()

View File

@ -0,0 +1,140 @@
package com.pitchedapps.frost.views
import android.content.Context
import android.os.Build
import android.support.v4.widget.SwipeRefreshLayout
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.ProgressBar
import ca.allanwang.kau.utils.*
import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.FrostContentContainer
import com.pitchedapps.frost.contracts.FrostContentCore
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.web.WEB_LOAD_DELAY
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject
class FrostContentWeb @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0
) : FrostContentView<FrostWebView>(context, attrs, defStyleAttr, defStyleRes) {
override val layoutRes: Int = R.layout.view_content_base_web
}
class FrostContentRecycler @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0
) : FrostContentView<FrostRecyclerView>(context, attrs, defStyleAttr, defStyleRes) {
override val layoutRes: Int = R.layout.view_content_base_recycler
}
abstract class FrostContentView<out T> @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes),
FrostContentParent where T : View, T : FrostContentCore {
private val refresh: SwipeRefreshLayout by bindView(R.id.content_refresh)
private val progress: ProgressBar by bindView(R.id.content_progress)
val coreView: T by bindView(R.id.content_core)
override val core: FrostContentCore
get() = coreView
override val progressObservable: PublishSubject<Int> = PublishSubject.create()
override val refreshObservable: PublishSubject<Boolean> = PublishSubject.create()
override val titleObservable: BehaviorSubject<String> = BehaviorSubject.create()
override lateinit var baseUrl: String
override var baseEnum: FbItem? = null
protected abstract val layoutRes: Int
/**
* Sets up everything
* Called by [bind]
*/
protected fun init() {
inflate(context, layoutRes, this)
coreView.parent = this
// bind observables
progressObservable.observeOn(AndroidSchedulers.mainThread()).subscribe {
progress.invisibleIf(it == 100)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
progress.setProgress(it, true)
else
progress.progress = it
}
refreshObservable.observeOn(AndroidSchedulers.mainThread()).subscribe {
refresh.isRefreshing = it
refresh.isEnabled = true
}
refresh.setOnRefreshListener { coreView.reload(true) }
reloadThemeSelf()
}
override fun bind(container: FrostContentContainer) {
baseUrl = container.baseUrl
baseEnum = container.baseEnum
init()
core.bind(container)
}
override fun reloadTheme() {
reloadThemeSelf()
coreView.reloadTheme()
}
override fun reloadTextSize() {
coreView.reloadTextSize()
}
override fun reloadThemeSelf() {
progress.tint(Prefs.textColor.withAlpha(180))
refresh.setColorSchemeColors(Prefs.iconColor)
refresh.setProgressBackgroundColorSchemeColor(Prefs.headerColor.withAlpha(255))
}
override fun reloadTextSizeSelf() {
// intentionally blank
}
override fun destroy() {
titleObservable.onComplete()
progressObservable.onComplete()
refreshObservable.onComplete()
core.destroy()
}
/**
* Hook onto the refresh observable for one cycle
* Animate toggles between the fancy ripple and the basic fade
* The cycle only starts on the first load since there may have been another process when this is registered
*/
override fun registerTransition(animate: Boolean) {
with(coreView) {
var dispose: Disposable? = null
var loading = false
dispose = refreshObservable.subscribeOn(AndroidSchedulers.mainThread()).subscribe {
if (it) {
loading = true
if (isVisible) fadeOut(duration = 200L)
} else if (loading) {
dispose?.dispose()
if (animate && Prefs.animate) circularReveal(offset = WEB_LOAD_DELAY)
else fadeIn(duration = 100L)
}
}
}
}
}

View File

@ -0,0 +1,103 @@
package com.pitchedapps.frost.views
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.view.View
import com.pitchedapps.frost.contracts.FrostContentContainer
import com.pitchedapps.frost.contracts.FrostContentCore
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.fragments.RecyclerContentContract
import com.pitchedapps.frost.utils.L
import java.lang.ref.WeakReference
/**
* Created by Allan Wang on 2017-05-29.
*
*/
class FrostRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr),
FrostContentCore {
override fun reload(animate: Boolean) = reloadBase(animate)
override lateinit var parent: FrostContentParent
override val currentUrl: String
get() = parent.baseUrl
lateinit var recyclerContract: WeakReference<RecyclerContentContract>
override fun bind(container: FrostContentContainer): View {
if (container !is RecyclerContentContract)
throw IllegalStateException("FrostRecyclerView must bind to a container that is a RecyclerContentContract")
this.recyclerContract = WeakReference(container)
container.bind(this)
return this
}
init {
isNestedScrollingEnabled = true
}
override fun reloadBase(animate: Boolean) {
val contract = recyclerContract.get()
if (contract == null) {
L.eThrow("Attempted to reload with invalid contract")
return
}
contract.reload({ parent.progressObservable.onNext(it) }) {
parent.progressObservable.onNext(100)
parent.refreshObservable.onNext(false)
}
}
override fun clearHistory() {
// intentionally blank
}
override fun destroy() {
// todo see if any
}
override fun onBackPressed() = false
/**
* If webview is already at the top, refresh
* Otherwise scroll to top
*/
override fun onTabClicked() {
if (scrollY < 5) reloadBase(true)
else scrollToTop()
}
private fun scrollToTop() {
stopScroll()
smoothScrollToPosition(0)
}
override var active: Boolean = true
set(value) {
if (field == value) return
field = value
// todo
}
override fun reloadTheme() {
reloadThemeSelf()
}
override fun reloadThemeSelf() {
reload(false) // todo see if there's a better solution
}
override fun reloadTextSize() {
reloadTextSizeSelf()
}
override fun reloadTextSizeSelf() {
// todo
}
}

View File

@ -0,0 +1,144 @@
package com.pitchedapps.frost.views
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import android.view.animation.DecelerateInterpolator
import com.pitchedapps.frost.contracts.FrostContentContainer
import com.pitchedapps.frost.contracts.FrostContentCore
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostDownload
import com.pitchedapps.frost.web.*
/**
* Created by Allan Wang on 2017-05-29.
*
*/
class FrostWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : NestedWebView(context, attrs, defStyleAttr),
FrostContentCore {
override fun reload(animate: Boolean) {
parent.registerTransition(animate)
super.reload()
}
override lateinit var parent: FrostContentParent
internal lateinit var frostWebClient: FrostWebViewClient
override val currentUrl: String
get() = url ?: ""
@SuppressLint("SetJavaScriptEnabled")
override fun bind(container: FrostContentContainer): View {
with(settings) {
javaScriptEnabled = true
if (parent.baseUrl.shouldUseBasicAgent)
userAgentString = USER_AGENT_BASIC
allowFileAccess = true
textZoom = Prefs.webTextScaling
}
setLayerType(LAYER_TYPE_HARDWARE, null)
// attempt to get custom client; otherwise fallback to original
frostWebClient = (container as? WebFragment)?.client(this) ?: FrostWebViewClient(this)
webViewClient = frostWebClient
webChromeClient = FrostChromeClient(this)
addJavascriptInterface(FrostJSI(this), "Frost")
setBackgroundColor(Color.TRANSPARENT)
setDownloadListener(context::frostDownload)
return this
}
/**
* Wrapper to the main userAgentString to cache it.
* This decouples it from the UiThread
*
* Note that this defaults to null, but the main purpose is to
* check if we've set our own agent.
*
* A null value may be interpreted as the default value
*/
var userAgentString: String? = null
set(value) {
field = value
settings.userAgentString = value
}
init {
isNestedScrollingEnabled = true
}
fun loadUrl(url: String?, animate: Boolean) {
if (url == null) return
parent.registerTransition(animate)
super.loadUrl(url)
}
override fun reloadBase(animate: Boolean) {
loadUrl(parent.baseUrl, animate)
}
override fun onBackPressed(): Boolean {
if (canGoBack()) {
goBack()
return true
}
return false
}
/**
* If webview is already at the top, refresh
* Otherwise scroll to top
*/
override fun onTabClicked() {
if (scrollY < 5) reloadBase(true)
else scrollToTop()
}
private fun scrollToTop() {
flingScroll(0, 0) // stop fling
if (scrollY > 10000) {
scrollTo(0, 0)
} else {
ValueAnimator.ofInt(scrollY, 0).apply {
duration = Math.min(scrollY, 500).toLong()
interpolator = DecelerateInterpolator()
addUpdateListener { scrollY = it.animatedValue as Int }
start()
}
}
}
override var active: Boolean = true
set(value) {
if (field == value) return
field = value
// todo
}
override fun reloadTheme() {
reloadThemeSelf()
}
override fun reloadThemeSelf() {
reload(false) // todo see if there's a better solution
}
override fun reloadTextSize() {
reloadTextSizeSelf()
}
override fun reloadTextSizeSelf() {
settings.textZoom = Prefs.webTextScaling
}
}

View File

@ -79,7 +79,7 @@ class KeywordItem(val keyword: String) : AbstractItem<KeywordItem, KeywordItem.V
override fun getLayoutRes(): Int = R.layout.item_keyword
override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
override fun bindView(holder: ViewHolder, payloads: MutableList<Any>) {
super.bindView(holder, payloads)
holder.text.text = keyword
}

View File

@ -5,9 +5,10 @@ import android.webkit.*
import ca.allanwang.kau.permissions.PERMISSION_ACCESS_FINE_LOCATION
import ca.allanwang.kau.permissions.kauRequestPermissions
import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.ActivityWebContract
import com.pitchedapps.frost.contracts.ActivityContract
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.frostSnackbar
import com.pitchedapps.frost.views.FrostWebView
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
@ -45,12 +46,12 @@ class HeadlessChromeClient : WebChromeClient() {
/**
* The default chrome client
*/
class FrostChromeClient(webCore: FrostWebViewCore) : WebChromeClient() {
class FrostChromeClient(web: FrostWebView) : WebChromeClient() {
val progressObservable: Subject<Int> = webCore.progressObservable
val titleObservable: BehaviorSubject<String> = webCore.titleObservable
val activityContract = (webCore.context as? ActivityWebContract)
val context = webCore.context!!
private val progress: Subject<Int> = web.parent.progressObservable
private val title: BehaviorSubject<String> = web.parent.titleObservable
private val activity = (web.context as? ActivityContract)
private val context = web.context!!
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
if (consoleBlacklist.any { consoleMessage.message().contains(it) }) return true
@ -60,18 +61,18 @@ class FrostChromeClient(webCore: FrostWebViewCore) : WebChromeClient() {
override fun onReceivedTitle(view: WebView, title: String) {
super.onReceivedTitle(view, title)
if (title.contains("http") || titleObservable.value == title) return
titleObservable.onNext(title)
if (title.contains("http") || this.title.value == title) return
this.title.onNext(title)
}
override fun onProgressChanged(view: WebView, newProgress: Int) {
super.onProgressChanged(view, newProgress)
progressObservable.onNext(newProgress)
progress.onNext(newProgress)
}
override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback<Array<Uri>?>, fileChooserParams: FileChooserParams): Boolean {
activityContract?.openFileChooser(filePathCallback, fileChooserParams) ?: webView.frostSnackbar(R.string.file_chooser_not_found)
return activityContract != null
activity?.openFileChooser(filePathCallback, fileChooserParams) ?: webView.frostSnackbar(R.string.file_chooser_not_found)
return activity != null
}
override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {

View File

@ -1,31 +1,24 @@
package com.pitchedapps.frost.web
import android.content.Context
import android.support.v4.widget.SwipeRefreshLayout
import android.webkit.JavascriptInterface
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.contracts.VideoViewHolder
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.views.FrostWebView
import io.reactivex.subjects.Subject
/**
* Created by Allan Wang on 2017-06-01.
*/
class FrostJSI(val webView: FrostWebViewCore) {
class FrostJSI(val web: FrostWebView) {
val context: Context
get() = webView.context
val activity: MainActivity?
get() = (context as? MainActivity)
val headerObservable: Subject<String>? = activity?.headerBadgeObservable
val cookies: ArrayList<CookieModel>
get() = activity?.cookies() ?: arrayListOf()
private val context = web.context
private val activity = context as? MainActivity
private val header: Subject<String>? = activity?.headerBadgeObservable
private val cookies = activity?.cookies() ?: arrayListOf()
/**
* Attempts to load the url in an overlay
@ -34,12 +27,12 @@ class FrostJSI(val webView: FrostWebViewCore) {
*/
@JavascriptInterface
fun loadUrl(url: String?): Boolean
= if (url == null) false else webView.requestWebOverlay(url)
= if (url == null) false else web.requestWebOverlay(url)
@JavascriptInterface
fun loadVideo(url: String?, isGif: Boolean) {
if (url != null)
webView.post {
web.post {
(context as? VideoViewHolder)?.showVideo(url, isGif)
?: L.d("Could not load video; contract not implemented")
}
@ -48,9 +41,9 @@ class FrostJSI(val webView: FrostWebViewCore) {
@JavascriptInterface
fun reloadBaseUrl(animate: Boolean) {
L.d("FrostJSI reload")
webView.post {
webView.stopLoading()
webView.loadBaseUrl(animate)
web.post {
web.stopLoading()
web.reloadBase(animate)
}
}
@ -58,7 +51,7 @@ class FrostJSI(val webView: FrostWebViewCore) {
fun contextMenu(url: String, text: String?) {
if (!text.isIndependent) return
//url will be formatted through webcontext
webView.post { context.showWebContextMenu(WebContext(url, text)) }
web.post { context.showWebContextMenu(WebContext(url, text)) }
}
/**
@ -75,7 +68,7 @@ class FrostJSI(val webView: FrostWebViewCore) {
*/
@JavascriptInterface
fun disableSwipeRefresh(disable: Boolean) {
webView.post { (webView.parent as? SwipeRefreshLayout)?.isEnabled = !disable }
web.post { (web.parent as? SwipeRefreshLayout)?.isEnabled = !disable }
}
@JavascriptInterface
@ -93,19 +86,19 @@ class FrostJSI(val webView: FrostWebViewCore) {
@JavascriptInterface
fun emit(flag: Int) {
webView.post { webView.frostWebClient.emit(flag) }
web.post { web.frostWebClient.emit(flag) }
}
@JavascriptInterface
fun handleHtml(html: String?) {
html ?: return
webView.post { webView.frostWebClient.handleHtml(html) }
web.post { web.frostWebClient.handleHtml(html) }
}
@JavascriptInterface
fun handleHeader(html: String?) {
html ?: return
headerObservable?.onNext(html)
header?.onNext(html)
}
}

View File

@ -9,6 +9,7 @@ import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.views.FrostWebView
import org.jetbrains.anko.runOnUiThread
/**
@ -27,7 +28,7 @@ import org.jetbrains.anko.runOnUiThread
* whether the user agent string should be changed. All propagated results will return false,
* as we have no need of sending a new intent to the same activity
*/
fun FrostWebViewCore.requestWebOverlay(url: String): Boolean {
fun FrostWebView.requestWebOverlay(url: String): Boolean {
if (url == "#" || !url.isIndependent) {
L.i("Forbid overlay switch", url)
return false

View File

@ -1,92 +0,0 @@
package com.pitchedapps.frost.web
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.support.v4.widget.SwipeRefreshLayout
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.ProgressBar
import ca.allanwang.kau.utils.*
import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostDownload
import io.reactivex.android.schedulers.AndroidSchedulers
/**
* Created by Allan Wang on 2017-06-01.
*/
class FrostWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), SwipeRefreshLayout.OnRefreshListener {
val refresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh)
val web: FrostWebViewCore by bindView(R.id.frost_webview_core)
val progress: ProgressBar by bindView(R.id.progress_bar)
init {
inflate(getContext(), R.layout.swipe_webview, this)
progress.tint(Prefs.textColor.withAlpha(180))
refresh.setColorSchemeColors(Prefs.iconColor)
refresh.setProgressBackgroundColorSchemeColor(Prefs.headerColor.withAlpha(255))
web.progressObservable.observeOn(AndroidSchedulers.mainThread()).subscribe {
progress.invisibleIf(it == 100)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progress.setProgress(it, true)
else progress.progress = it
}
web.refreshObservable.observeOn(AndroidSchedulers.mainThread()).subscribe {
refresh.isRefreshing = it
refresh.isEnabled = true
}
refresh.setOnRefreshListener(this)
addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View) {
web.visible()
}
override fun onViewAttachedToWindow(v: View) {}
})
}
@SuppressLint("SetJavaScriptEnabled")
fun setupWebview(url: String, enum: FbItem? = null) {
with(web) {
baseUrl = url
baseEnum = enum
with(settings) {
javaScriptEnabled = true
if (url.shouldUseBasicAgent)
userAgentString = USER_AGENT_BASIC
allowFileAccess = true
textZoom = Prefs.webTextScaling
}
setLayerType(View.LAYER_TYPE_HARDWARE, null)
frostWebClient = baseEnum?.webClient?.invoke(this) ?: FrostWebViewClient(this)
webViewClient = frostWebClient
webChromeClient = FrostChromeClient(this)
addJavascriptInterface(FrostJSI(this), "Frost")
setBackgroundColor(Color.TRANSPARENT)
setDownloadListener(context::frostDownload)
}
}
//Some urls have postJavascript injections so make sure we load the base url
override fun onRefresh() {
when (web.baseUrl) {
FbItem.MENU.url -> web.loadBaseUrl(true)
else -> web.reload(true)
}
}
fun onBackPressed(): Boolean {
if (web.canGoBack()) {
web.goBack()
return true
}
return false
}
}

View File

@ -15,6 +15,7 @@ import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.injectors.*
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import com.pitchedapps.frost.views.FrostWebView
import io.reactivex.subjects.Subject
import org.jetbrains.anko.withAlpha
@ -38,16 +39,16 @@ open class BaseWebViewClient : WebViewClient() {
/**
* The default webview client
*/
open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient() {
open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() {
val refreshObservable: Subject<Boolean> = webCore.refreshObservable
val isMain = webCore.baseEnum != null
private val refresh: Subject<Boolean> = web.parent.refreshObservable
private val isMain = web.parent.baseEnum != null
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
if (url == null) return
L.d("FWV Loading", url)
refreshObservable.onNext(true)
refresh.onNext(true)
}
fun launchLogin(c: Context) {
@ -58,10 +59,10 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
}
private fun injectBackgroundColor() {
webCore.setBackgroundColor(
web.setBackgroundColor(
when {
isMain -> Color.TRANSPARENT
webCore.url.isFacebookUrl -> Prefs.bgColor.withAlpha(255)
web.url.isFacebookUrl -> Prefs.bgColor.withAlpha(255)
else -> Color.WHITE
}
)
@ -80,7 +81,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!Prefs.showSuggestedFriends && IS_FROST_PRO),
CssHider.SUGGESTED_GROUPS.maybe(!Prefs.showSuggestedGroups && IS_FROST_PRO),
Prefs.themeInjector,
CssHider.NON_RECENT.maybe((webCore.url?.contains("?sk=h_chr") ?: false)
CssHider.NON_RECENT.maybe((web.url?.contains("?sk=h_chr") ?: false)
&& Prefs.aggressiveRecents))
}
@ -88,7 +89,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
url ?: return
L.d("Page finished", url)
if (!url.isFacebookUrl) {
refreshObservable.onNext(false)
refresh.onNext(false)
return
}
onPageFinishedActions(url)
@ -96,22 +97,22 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
open internal fun onPageFinishedActions(url: String) {
if (url.startsWith("${FbItem.MESSAGES.url}/read/") && Prefs.messageScrollToBottom)
webCore.pageDown(true)
web.pageDown(true)
injectAndFinish()
}
internal fun injectAndFinish() {
L.d("Page finished reveal")
refreshObservable.onNext(false)
refresh.onNext(false)
injectBackgroundColor()
webCore.jsInject(
web.jsInject(
JsActions.LOGIN_CHECK,
JsAssets.CLICK_A,
JsAssets.TEXTAREA_LISTENER,
CssHider.ADS.maybe(!Prefs.showFacebookAds && IS_FROST_PRO),
JsAssets.CONTEXT_A,
JsAssets.MEDIA,
JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null)
JsAssets.HEADER_BADGES.maybe(web.parent.baseEnum != null)
)
}
@ -130,13 +131,13 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
*/
private fun launchRequest(request: WebResourceRequest): Boolean {
L.d("Launching Url", request.url?.toString() ?: "null")
return webCore.requestWebOverlay(request.url.toString())
return web.requestWebOverlay(request.url.toString())
}
private fun launchImage(url: String, text: String? = null): Boolean {
L.d("Launching Image", url)
webCore.context.launchImageActivity(url, text)
if (webCore.canGoBack()) webCore.goBack()
web.context.launchImageActivity(url, text)
if (web.canGoBack()) web.goBack()
return true
}
@ -161,7 +162,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
/**
* Client variant for the menu view
*/
class FrostWebViewClientMenu(webCore: FrostWebViewCore) : FrostWebViewClient(webCore) {
class FrostWebViewClientMenu(web: FrostWebView) : FrostWebViewClient(web) {
private val String.shouldInjectMenu
get() = when (removePrefix(FB_URL_BASE)) {

View File

@ -1,203 +0,0 @@
package com.pitchedapps.frost.web
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.support.v4.view.NestedScrollingChild
import android.support.v4.view.NestedScrollingChildHelper
import android.support.v4.view.ViewCompat
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.animation.DecelerateInterpolator
import android.webkit.WebView
import ca.allanwang.kau.utils.circularReveal
import ca.allanwang.kau.utils.fadeIn
import ca.allanwang.kau.utils.fadeOut
import ca.allanwang.kau.utils.isVisible
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.utils.Prefs
import io.reactivex.Scheduler
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject
/**
* Created by Allan Wang on 2017-05-29.
*
*/
class FrostWebViewCore @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr), NestedScrollingChild {
private val childHelper = NestedScrollingChildHelper(this)
private var lastY: Int = 0
private val scrollOffset = IntArray(2)
private val scrollConsumed = IntArray(2)
private var nestedOffsetY: Int = 0
val progressObservable: PublishSubject<Int> // Keeps track of every progress change
val refreshObservable: PublishSubject<Boolean> // Only emits on page loads
val titleObservable: BehaviorSubject<String> // Only emits on different non http titles
var baseUrl: String? = null
var baseEnum: FbItem? = null //only viewpager items should pass the base enum
internal lateinit var frostWebClient: FrostWebViewClient
/**
* Wrapper to the main userAgentString to cache it.
* This decouples it from the UiThread
*
* Note that this defaults to null, but the main purpose is to
* check if we've set our own agent.
*
* A null value may be interpreted as the default value
*/
var userAgentString: String? = null
get() = field
set(value) {
field = value
settings.userAgentString = value
}
init {
isNestedScrollingEnabled = true
progressObservable = PublishSubject.create<Int>()
refreshObservable = PublishSubject.create<Boolean>()
titleObservable = BehaviorSubject.create<String>()
}
fun loadUrl(url: String?, animate: Boolean) {
if (url == null) return
registerTransition(animate)
super.loadUrl(url)
}
fun reload(animate: Boolean) {
registerTransition(animate)
super.reload()
}
/**
* Hook onto the refresh observable for one cycle
* Animate toggles between the fancy ripple and the basic fade
* The cycle only starts on the first load since there may have been another process when this is registered
*/
fun registerTransition(animate: Boolean) {
var dispose: Disposable? = null
var loading = false
dispose = refreshObservable.subscribeOn(AndroidSchedulers.mainThread()).subscribe {
if (it) {
loading = true
if (isVisible) fadeOut(duration = 200L)
} else if (loading) {
dispose?.dispose()
if (animate && Prefs.animate) circularReveal(offset = WEB_LOAD_DELAY)
else fadeIn(duration = 100L)
}
}
}
fun loadBaseUrl(animate: Boolean = true) {
loadUrl(baseUrl, animate)
}
fun addTitleListener(subscriber: (title: String) -> Unit, scheduler: Scheduler = AndroidSchedulers.mainThread()): Disposable
= titleObservable.observeOn(scheduler).subscribe(subscriber)
/**
* Handle nested scrolling against SwipeRecyclerView
* Courtesy of takahirom
*
* https://github.com/takahirom/webview-in-coordinatorlayout/blob/master/app/src/main/java/com/github/takahirom/webview_in_coodinator_layout/NestedWebView.java
*/
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean {
val event = MotionEvent.obtain(ev)
val action = event.action
if (action == MotionEvent.ACTION_DOWN)
nestedOffsetY = 0
val eventY = event.y.toInt()
event.offsetLocation(0f, nestedOffsetY.toFloat())
val returnValue: Boolean
when (action) {
MotionEvent.ACTION_MOVE -> {
var deltaY = lastY - eventY
// NestedPreScroll
if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
deltaY -= scrollConsumed[1]
event.offsetLocation(0f, -scrollOffset[1].toFloat())
nestedOffsetY += scrollOffset[1]
}
lastY = eventY - scrollOffset[1]
returnValue = super.onTouchEvent(event)
// NestedScroll
if (dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
event.offsetLocation(0f, scrollOffset[1].toFloat())
nestedOffsetY += scrollOffset[1]
lastY -= scrollOffset[1]
}
}
MotionEvent.ACTION_DOWN -> {
returnValue = super.onTouchEvent(event)
lastY = eventY
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
returnValue = super.onTouchEvent(event)
stopNestedScroll()
}
else -> return false
}
return returnValue
}
/**
* If webview is already at the top, refresh
* Otherwise scroll to top
*/
fun scrollOrRefresh() {
if (scrollY < 5) loadBaseUrl()
else scrollToTop()
}
fun scrollToTop() {
flingScroll(0, 0) // stop fling
if (scrollY > 10000) {
scrollTo(0, 0)
} else {
ValueAnimator.ofInt(scrollY, 0).apply {
duration = Math.min(scrollY, 500).toLong()
interpolator = DecelerateInterpolator()
addUpdateListener { scrollY = it.animatedValue as Int }
start()
}
}
}
// Nested Scroll implements
override fun setNestedScrollingEnabled(enabled: Boolean) {
childHelper.isNestedScrollingEnabled = enabled
}
override fun isNestedScrollingEnabled() = childHelper.isNestedScrollingEnabled
override fun startNestedScroll(axes: Int) = childHelper.startNestedScroll(axes)
override fun stopNestedScroll() = childHelper.stopNestedScroll()
override fun hasNestedScrollingParent() = childHelper.hasNestedScrollingParent()
override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?)
= childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?)
= childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean)
= childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float)
= childHelper.dispatchNestedPreFling(velocityX, velocityY)
}

View File

@ -0,0 +1,113 @@
package com.pitchedapps.frost.web
import android.annotation.SuppressLint
import android.content.Context
import android.support.v4.view.NestedScrollingChild
import android.support.v4.view.NestedScrollingChildHelper
import android.support.v4.view.ViewCompat
import android.util.AttributeSet
import android.view.MotionEvent
import android.webkit.WebView
/**
* Created by Allan Wang on 20/12/17.
*
* Webview extension that handles nested scrolls
*/
open class NestedWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr), NestedScrollingChild {
private lateinit var childHelper: NestedScrollingChildHelper
private var lastY: Int = 0
private val scrollOffset = IntArray(2)
private val scrollConsumed = IntArray(2)
private var nestedOffsetY: Int = 0
init {
init()
}
fun init() {
// To avoid leaking constructor
childHelper = NestedScrollingChildHelper(this)
}
/**
* Handle nested scrolling against SwipeRecyclerView
* Courtesy of takahirom
*
* https://github.com/takahirom/webview-in-coordinatorlayout/blob/master/app/src/main/java/com/github/takahirom/webview_in_coodinator_layout/NestedWebView.java
*/
@SuppressLint("ClickableViewAccessibility")
override final fun onTouchEvent(ev: MotionEvent): Boolean {
val event = MotionEvent.obtain(ev)
val action = event.action
if (action == MotionEvent.ACTION_DOWN)
nestedOffsetY = 0
val eventY = event.y.toInt()
event.offsetLocation(0f, nestedOffsetY.toFloat())
val returnValue: Boolean
when (action) {
MotionEvent.ACTION_MOVE -> {
var deltaY = lastY - eventY
// NestedPreScroll
if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
deltaY -= scrollConsumed[1]
event.offsetLocation(0f, -scrollOffset[1].toFloat())
nestedOffsetY += scrollOffset[1]
}
lastY = eventY - scrollOffset[1]
returnValue = super.onTouchEvent(event)
// NestedScroll
if (dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
event.offsetLocation(0f, scrollOffset[1].toFloat())
nestedOffsetY += scrollOffset[1]
lastY -= scrollOffset[1]
}
}
MotionEvent.ACTION_DOWN -> {
returnValue = super.onTouchEvent(event)
lastY = eventY
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
returnValue = super.onTouchEvent(event)
stopNestedScroll()
}
else -> return false
}
return returnValue
}
/*
* ---------------------------------------------
* Nested Scrolling Content
* ---------------------------------------------
*/
override final fun setNestedScrollingEnabled(enabled: Boolean) {
childHelper.isNestedScrollingEnabled = enabled
}
override final fun isNestedScrollingEnabled() = childHelper.isNestedScrollingEnabled
override final fun startNestedScroll(axes: Int) = childHelper.startNestedScroll(axes)
override final fun stopNestedScroll() = childHelper.stopNestedScroll()
override final fun hasNestedScrollingParent() = childHelper.hasNestedScrollingParent()
override final fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?)
= childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
override final fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?)
= childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
override final fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean)
= childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
override final fun dispatchNestedPreFling(velocityX: Float, velocityY: Float)
= childHelper.dispatchNestedPreFling(velocityX, velocityY)
}

View File

@ -17,10 +17,23 @@
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay" />
<com.pitchedapps.frost.web.FrostWebView
android:id="@+id/overlay_frost_webview"
<include
layout="@layout/view_content_web"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize" />
<!--<com.pitchedapps.frost.views.FrostRefreshView-->
<!--android:id="@+id/overlay_frost_refresh_view"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="match_parent"-->
<!--android:layout_marginTop="?attr/actionBarSize">-->
<!--<com.pitchedapps.frost.views.FrostWebView-->
<!--android:id="@+id/overlay_frost_web_view"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="match_parent" />-->
<!--</com.pitchedapps.frost.views.FrostRefreshView>-->
</android.support.design.widget.CoordinatorLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@id/content_refresh"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.pitchedapps.frost.views.FrostRecyclerView
android:id="@id/content_core"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.v4.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@id/content_progress"
style="@style/FrostProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top" />
</FrameLayout>

View File

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:id="@id/content_refresh"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.pitchedapps.frost.web.FrostWebViewCore
android:id="@+id/frost_webview_core"
<com.pitchedapps.frost.views.FrostWebView
android:id="@id/content_core"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
@ -22,9 +22,10 @@
</android.support.v4.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:id="@id/content_progress"
style="@style/FrostProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top" />
</FrameLayout>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<com.pitchedapps.frost.views.FrostContentRecycler xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<com.pitchedapps.frost.views.FrostContentWeb xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -15,4 +15,9 @@
<item name="action_notification" type="id" />
<item name="action_messages" type="id" />
<item name="action_frost" type="id" />
<item name="content_progress" type="id" />
<item name="content_refresh" type="id" />
<item name="content_core" type="id" />
<item name="content_frame" type="id" />
</resources>