aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt402
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt416
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt77
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt14
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/DynamicUiContract.kt30
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt140
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostObservables.kt31
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostThemable.kt29
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostUrlData.kt25
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/WebContract.kt8
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt27
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt10
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt234
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt92
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt133
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt12
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt16
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt8
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt140
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt103
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt144
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt23
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt39
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt92
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt33
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt203
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt113
38 files changed, 1662 insertions, 968 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
new file mode 100644
index 00000000..389ff88e
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
@@ -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)
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
index 1ba7f4c3..f72807d1 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
@@ -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)
-
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
index f17ccf20..19f5102a 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
@@ -161,7 +161,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IabSettings() {
}
fun shouldRestartMain() {
- setFrostResult(MainActivity.REQUEST_RESTART)
+ setFrostResult(REQUEST_RESTART)
}
@SuppressLint("MissingSuperCall")
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
index c41964cd..0dbbacbc 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
@@ -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
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
new file mode 100644
index 00000000..f51c4e53
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
@@ -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)
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/DynamicUiContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/DynamicUiContract.kt
new file mode 100644
index 00000000..303c64b3
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/DynamicUiContract.kt
@@ -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()
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt
new file mode 100644
index 00000000..681636c4
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt
@@ -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()
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostObservables.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostObservables.kt
new file mode 100644
index 00000000..882b67a0
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostObservables.kt
@@ -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
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostThemable.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostThemable.kt
new file mode 100644
index 00000000..3322f62e
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostThemable.kt
@@ -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) }
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostUrlData.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostUrlData.kt
new file mode 100644
index 00000000..18467fa4
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostUrlData.kt
@@ -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
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/WebContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/WebContract.kt
deleted file mode 100644
index 2485a468..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/WebContract.kt
+++ /dev/null
@@ -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 \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt b/app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt
index cc71b19e..8f26e152 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt
@@ -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)
}
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
index 32955f06..cc2ca556 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
@@ -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"),
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
new file mode 100644
index 00000000..498164c0
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
@@ -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)
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
new file mode 100644
index 00000000..62b1de33
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
@@ -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)
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt
deleted file mode 100644
index f1b76e57..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt
+++ /dev/null
@@ -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()
-} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
index 4fed2db9..4a5bff10 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
@@ -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
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt
index bae8ac7a..39a39232 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt
@@ -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() {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt
index 9e247f1e..186633e5 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt
@@ -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
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt
index 2c229830..7305f9ef 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt
@@ -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) })))
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt
index db2eea4b..4d0cd9d8 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt
@@ -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
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt
index ed011af9..06489033 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt
@@ -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()
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt
index 2a0f913c..00ce5116 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt
@@ -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()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt
new file mode 100644
index 00000000..61ba4a09
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt
@@ -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 \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
index dbace074..94bf0016 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
@@ -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")
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt
index 8ddb46d2..71c6df36 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt
@@ -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()
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
index 4b6c9e4e..2ab1d572 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
@@ -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()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt
new file mode 100644
index 00000000..58449de3
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt
@@ -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)
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt
new file mode 100644
index 00000000..436f8b00
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt
@@ -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
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
new file mode 100644
index 00000000..e6e1f0e2
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
@@ -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
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
index 25079834..9edd671b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
@@ -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
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
index 2fa80830..344fcb27 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
@@ -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) {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
index 6bdb459e..e8135f5b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
@@ -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)
}
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt
index d1f144a6..9255b5bb 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt
@@ -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
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt
deleted file mode 100644
index f6d64ab7..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt
+++ /dev/null
@@ -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
- }
-} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt
index 5fc1ab27..71c71b66 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt
@@ -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)) {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt
deleted file mode 100644
index 15383a50..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt
+++ /dev/null
@@ -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)
-
-} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt
new file mode 100644
index 00000000..f0bb6137
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt
@@ -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)
+} \ No newline at end of file