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:
parent
82f9aca964
commit
d683cae6ff
@ -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)
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IabSettings() {
|
||||
}
|
||||
|
||||
fun shouldRestartMain() {
|
||||
setFrostResult(MainActivity.REQUEST_RESTART)
|
||||
setFrostResult(REQUEST_RESTART)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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) }
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
@ -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)
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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) })))
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
16
app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt
Normal file
16
app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt
Normal 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
|
@ -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")
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
144
app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
Normal file
144
app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)) {
|
||||
|
@ -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)
|
||||
|
||||
}
|
113
app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt
Normal file
113
app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt
Normal 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)
|
||||
}
|
@ -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>
|
||||
|
32
app/src/main/res/layout/view_content_base_recycler.xml
Normal file
32
app/src/main/res/layout/view_content_base_recycler.xml
Normal 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>
|
@ -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>
|
4
app/src/main/res/layout/view_content_recycler.xml
Normal file
4
app/src/main/res/layout/view_content_recycler.xml
Normal 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" />
|
4
app/src/main/res/layout/view_content_web.xml
Normal file
4
app/src/main/res/layout/view_content_web.xml
Normal 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" />
|
@ -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>
|
Loading…
Reference in New Issue
Block a user