From d683cae6ffe644a9f63eea6cf3b7e59d2bde617b Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 21 Dec 2017 02:16:34 -0500 Subject: Enhancement/fragment interface (#564) * Begin fragment interfaces and themable contracts * Prepare swiperefresh interface * Snapshot * Add compilable version * Revamp once more * Finalize layouts * Cleanup --- .../frost/activities/BaseMainActivity.kt | 402 ++++++++++++++++++++ .../pitchedapps/frost/activities/MainActivity.kt | 416 +-------------------- .../frost/activities/SettingsActivity.kt | 2 +- .../frost/activities/WebOverlayActivity.kt | 77 ++-- .../frost/contracts/ActivityContract.kt | 14 + .../frost/contracts/DynamicUiContract.kt | 30 ++ .../frost/contracts/FrostContentContract.kt | 140 +++++++ .../frost/contracts/FrostObservables.kt | 31 ++ .../pitchedapps/frost/contracts/FrostThemable.kt | 29 ++ .../pitchedapps/frost/contracts/FrostUrlData.kt | 25 ++ .../com/pitchedapps/frost/contracts/WebContract.kt | 8 - .../com/pitchedapps/frost/enums/OverlayContext.kt | 27 +- .../com/pitchedapps/frost/facebook/FbItem.kt | 10 +- .../pitchedapps/frost/fragments/FragmentBase.kt | 234 ++++++++++++ .../frost/fragments/FragmentContract.kt | 92 +++++ .../com/pitchedapps/frost/fragments/WebFragment.kt | 133 ------- .../com/pitchedapps/frost/injectors/JsInjector.kt | 2 +- .../pitchedapps/frost/intro/IntroMainFragments.kt | 5 +- .../com/pitchedapps/frost/parsers/FrostParser.kt | 3 + .../com/pitchedapps/frost/settings/Appearance.kt | 7 +- .../com/pitchedapps/frost/settings/Behaviour.kt | 4 +- .../com/pitchedapps/frost/settings/Experimental.kt | 4 +- .../kotlin/com/pitchedapps/frost/settings/Feed.kt | 12 +- .../kotlin/com/pitchedapps/frost/utils/Const.kt | 16 + .../kotlin/com/pitchedapps/frost/utils/Prefs.kt | 2 + .../com/pitchedapps/frost/utils/iab/IABDialogs.kt | 8 +- .../com/pitchedapps/frost/views/AccountItem.kt | 2 +- .../pitchedapps/frost/views/FrostContentView.kt | 140 +++++++ .../pitchedapps/frost/views/FrostRecyclerView.kt | 103 +++++ .../com/pitchedapps/frost/views/FrostWebView.kt | 144 +++++++ .../kotlin/com/pitchedapps/frost/views/Keywords.kt | 2 +- .../pitchedapps/frost/web/FrostChromeClients.kt | 23 +- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 39 +- .../frost/web/FrostUrlOverlayValidator.kt | 3 +- .../com/pitchedapps/frost/web/FrostWebView.kt | 92 ----- .../pitchedapps/frost/web/FrostWebViewClients.kt | 33 +- .../com/pitchedapps/frost/web/FrostWebViewCore.kt | 203 ---------- .../com/pitchedapps/frost/web/NestedWebView.kt | 113 ++++++ 38 files changed, 1662 insertions(+), 968 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/contracts/DynamicUiContract.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostObservables.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostThemable.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostUrlData.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/contracts/WebContract.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/utils/Const.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt (limited to 'app/src/main/kotlin/com/pitchedapps/frost') 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>() + + 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?>, 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) : 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()!! + override val fragmentSubject = PublishSubject.create()!! var lastPosition = -1 val headerBadgeObservable = PublishSubject.create() 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>() - - 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?>, 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) : 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 + 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 + + /** + * Observable to get data on refresh progress, with range [0, 100] + */ + val progressObservable: PublishSubject + + /** + * Observable to get new title data (unique values only) + */ + val titleObservable: BehaviorSubject + + 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 + + /** + * Observable to get data on refresh progress, with range [0, 100] + */ + var progressObservable: PublishSubject + + /** + * Observable to get new title data (unique values only) + */ + var titleObservable: BehaviorSubject + + 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 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> : BaseFragment(), RecyclerContentContract { + + override val layoutRes: Int = R.layout.view_content_recycler + + /** + * The parser to make this all happen + */ + abstract val parser: FrostParser + + abstract val adapter: FastItemAdapter + + abstract fun toItems(data: T): List + + 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 validInjectors[i].inject(this, { observables[i].onSuccess(it) }) } } -fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract, callback: ((Array) -> Unit) = {}) = webCore.jsInject(*injectors, callback = callback) +fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract, callback: ((Array) -> 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> = 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 { /** @@ -37,6 +39,7 @@ interface FrostParser { } internal abstract class FrostParserBase : FrostParser { + 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 (R.layout.view_account, { ViewHolder(it) }, R.id.item_account) { - override fun bindView(viewHolder: ViewHolder, payloads: List?) { + override fun bindView(viewHolder: ViewHolder, payloads: MutableList) { 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(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(context, attrs, defStyleAttr, defStyleRes) { + + override val layoutRes: Int = R.layout.view_content_base_recycler + +} + +abstract class FrostContentView @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 = PublishSubject.create() + override val refreshObservable: PublishSubject = PublishSubject.create() + override val titleObservable: BehaviorSubject = 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 + + 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?) { + override fun bindView(holder: ViewHolder, payloads: MutableList) { 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 = webCore.progressObservable - val titleObservable: BehaviorSubject = webCore.titleObservable - val activityContract = (webCore.context as? ActivityWebContract) - val context = webCore.context!! + private val progress: Subject = web.parent.progressObservable + private val title: BehaviorSubject = 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?>, 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? = activity?.headerBadgeObservable - - val cookies: ArrayList - get() = activity?.cookies() ?: arrayListOf() + private val context = web.context + private val activity = context as? MainActivity + private val header: Subject? = 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 = webCore.refreshObservable - val isMain = webCore.baseEnum != null + private val refresh: Subject = 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 // Keeps track of every progress change - val refreshObservable: PublishSubject // Only emits on page loads - val titleObservable: BehaviorSubject // 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() - refreshObservable = PublishSubject.create() - titleObservable = BehaviorSubject.create() - } - - 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 -- cgit v1.2.3