From c4f936a8a43692cca387f00a25cd70bcad2764f2 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 25 Jun 2017 23:04:43 -0700 Subject: Half framework of the search view --- app/src/main/assets/js/search.js | 7 +- .../kotlin/com/pitchedapps/frost/MainActivity.kt | 49 +++++++- .../com/pitchedapps/frost/fragments/WebFragment.kt | 15 ++- .../com/pitchedapps/frost/injectors/JsAssets.kt | 2 +- .../pitchedapps/frost/web/FrostWebViewSearch.kt | 128 ++++++++++++++------- build.gradle | 2 +- 6 files changed, 152 insertions(+), 51 deletions(-) diff --git a/app/src/main/assets/js/search.js b/app/src/main/assets/js/search.js index 3d10e6f5..d3b319d4 100644 --- a/app/src/main/assets/js/search.js +++ b/app/src/main/assets/js/search.js @@ -1,10 +1,13 @@ //binds callbacks to an invisible webview to take in the search events console.log('Binding Search'); -var page = document.querySelector('#page'); +var _f_page = document.querySelector('#page'); +var _f_input = document.querySelector('#main-search-input') +if (!_f_page || !_f_input) return Frost.emit(1); +Frost.emit(0); var x = new MutationObserver(function(mutations) { Frost.handleHtml(page.innerHTML); }); -x.observe(page, { +x.observe(_f_page, { childList: true, subtree: true }); diff --git a/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt index b8feef0f..cccb3e0c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt @@ -9,6 +9,7 @@ import android.support.design.widget.FloatingActionButton import android.support.design.widget.Snackbar 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 @@ -39,13 +40,15 @@ import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL import com.pitchedapps.frost.fragments.WebFragment import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.views.BadgedIcon +import com.pitchedapps.frost.web.FrostWebViewSearch import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.PublishSubject import org.jsoup.Jsoup import java.util.concurrent.TimeUnit -class MainActivity : BaseActivity() { +class MainActivity : BaseActivity(), FrostWebViewSearch.SearchContract { lateinit var adapter: SectionsPagerAdapter val toolbar: Toolbar by bindView(R.id.toolbar) @@ -58,6 +61,16 @@ class MainActivity : BaseActivity() { var webFragmentObservable = PublishSubject.create()!! var lastPosition = -1 val headerBadgeObservable = PublishSubject.create() + var hiddenSearchView: FrostWebViewSearch? = null + var firstLoadFinished = false + set(value) { + L.d("First fragment load has finished") + field = value + if (value && hiddenSearchView == null) { + hiddenSearchView = FrostWebViewSearch(this, this) + currentFragment.frostWebView.addView(hiddenSearchView) + } + } companion object { const val FRAGMENT_REFRESH = 99 @@ -283,6 +296,21 @@ class MainActivity : BaseActivity() { onClick { _ -> onClick(); false } } + + /** + * Something happened where the normal search function won't work + * Fallback to overlay style + */ + override fun searchOverlayError() { + hiddenSearchView = null + //todo remove true searchview and add contract + } + + //todo add args + override fun emitSearchResponse() { + + } + fun refreshAll() { webFragmentObservable.onNext(FRAGMENT_REFRESH) } @@ -331,7 +359,24 @@ class MainActivity : BaseActivity() { inner class SectionsPagerAdapter(fm: FragmentManager, val pages: List) : FragmentPagerAdapter(fm) { - override fun getItem(position: Int) = WebFragment(pages[position], position) + 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 diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt index cd15c2c3..bd8b58ab 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt @@ -32,7 +32,7 @@ class WebFragment : Fragment() { putString(ARG_URL, data.url) putInt(ARG_POSITION, position) putSerializable(ARG_URL_ENUM, when (data) { - //If is feed, check if sorting method is specified + //If is feed, check if sorting method is specified FbTab.FEED -> when (FeedSort(Prefs.feedSort)) { FeedSort.DEFAULT -> data FeedSort.MOST_RECENT -> FbTab.FEED_MOST_RECENT @@ -48,9 +48,18 @@ class WebFragment : Fragment() { val url: String by lazy { arguments.getString(ARG_URL) } val urlEnum: FbTab by lazy { arguments.getSerializable(ARG_URL_ENUM) as FbTab } val position: Int by lazy { arguments.getInt(ARG_POSITION) } - lateinit private var frostWebView: FrostWebView + 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) @@ -61,6 +70,8 @@ class WebFragment : Fragment() { override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + onCreateRunnable?.invoke(this) + onCreateRunnable = null firstLoad() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt index a176042e..3ef1430d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt @@ -10,7 +10,7 @@ import com.pitchedapps.frost.utils.L * //TODO add folder mapping using Prefs */ enum class JsAssets : InjectorContract { - MENU, CLICK_A, CLICK_INTERCEPTOR, HEADER_BADGES + MENU, CLICK_A, CLICK_INTERCEPTOR, HEADER_BADGES, SEARCH ; var file = "${name.toLowerCase()}.min.js" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt index 0795efe2..85dfb254 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt @@ -2,80 +2,122 @@ package com.pitchedapps.frost.web import android.annotation.SuppressLint import android.content.Context -import android.graphics.Color -import android.support.v4.view.NestedScrollingChild -import android.util.AttributeSet +import android.os.Handler import android.view.View +import android.webkit.JavascriptInterface import android.webkit.WebView -import ca.allanwang.kau.utils.* +import android.webkit.WebViewClient import com.pitchedapps.frost.facebook.FbTab import com.pitchedapps.frost.facebook.USER_AGENT_BASIC -import com.pitchedapps.frost.utils.Prefs -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.subjects.BehaviorSubject +import com.pitchedapps.frost.injectors.JsAssets +import com.pitchedapps.frost.injectors.JsBuilder +import com.pitchedapps.frost.injectors.jsInject +import com.pitchedapps.frost.utils.L +import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.PublishSubject +import org.jsoup.Jsoup +import org.jsoup.nodes.TextNode +import java.util.concurrent.TimeUnit +@SuppressLint("ViewConstructor") /** * Created by Allan Wang on 2017-06-25. * * A bare bone search view meant solely to extract data from the web * This should be hidden */ -class FrostWebViewSearch (context: Context) : WebView(context) { - var baseUrl: String? = null - var baseEnum: FbTab? = null - internal var frostWebClient: FrostWebViewClient? = null +class FrostWebViewSearch(context: Context, val contract: SearchContract) : WebView(context) { + + val searchSubject = PublishSubject.create() init { - gone() +// gone() setupWebview() } @SuppressLint("SetJavaScriptEnabled") - fun setupWebview(url: String, enum: FbTab? = null) { - baseUrl = url - baseEnum = enum + fun setupWebview() { settings.javaScriptEnabled = true settings.userAgentString = USER_AGENT_BASIC -// settings.domStorageEnabled = true setLayerType(View.LAYER_TYPE_HARDWARE, null) - frostWebClient = baseEnum?.webClient?.invoke(this) ?: FrostWebViewClient(this) - webViewClient = frostWebClient - webChromeClient = FrostChromeClient(this) - addJavascriptInterface(FrostJSI(context, this), "Frost") - setBackgroundColor(Color.TRANSPARENT) + webViewClient = FrostWebViewClientSearch() + addJavascriptInterface(SearchJSI(), "Frost") + searchSubject.debounce(200, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread()) + .map { + Jsoup.parse(it).select("a:not([rel*='keywords(']):not([href=#])[rel]").map { + element -> +// L.d("Search element ${element.text()} ${element.textNodes().size} ${element.attr("href")}") + Pair(element.textNodes(), element.attr("href")) + }.filter { it.first.isNotEmpty() } + } + .subscribe { + content: List, String>> -> + content.forEach { +// L.e("Search result ${it.second}") + } + contract.emitSearchResponse() + } + reload() + Handler().postDelayed({ + query("hi") + }, 5000) } - fun loadUrl(url: String?, animate: Boolean) { - if (url == null) return - registerTransition(animate) - super.loadUrl(url) + override fun reload() { + super.loadUrl(FbTab.SEARCH.url) } - fun reload(animate: Boolean) { - registerTransition(animate) - super.reload() + fun query(input: String) { + JsBuilder().js("var input=document.getElementById('main-search-input');input.click(),input.value='$input';").build().inject(this) { + L.d("Searching for $input") + } } /** - * 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 + * Created by Allan Wang on 2017-05-31. + * + * Barebones client that does what [FrostWebViewSearch] needs */ - 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 = 150L) - else fadeIn(duration = 100L) + inner class FrostWebViewClientSearch : WebViewClient() { + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + L.i("Search Page finished $url") + view.jsInject(JsAssets.SEARCH) + } + } + + inner class SearchJSI { + @JavascriptInterface + fun handleHtml(html: String) { +// L.d("Search received response $html") + searchSubject.onNext(html) + } + + @JavascriptInterface + fun emit(flag: Int) { + L.d("Search flag") + when (flag) { + 0 -> { + JsBuilder().js("document.getElementById('main-search-input').click()").build().inject(this@FrostWebViewSearch) { + L.d("Search click") + } + } + 1 -> { //something is not found in the search view; this is effectively useless + L.d("Search subject error; reverting to full overlay") + searchSubject.onComplete() + contract.searchOverlayError() + } } } } + interface SearchContract { + fun searchOverlayError() + //todo add args + fun emitSearchResponse() + } } + + + diff --git a/build.gradle b/build.gradle index 083ca65f..df3b344f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.1.3' + ext.kotlin_version = '1.1.2-5' repositories { jcenter() maven { url 'https://maven.fabric.io/public' } -- cgit v1.2.3