/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.pitchedapps.frost.injectors
import android.webkit.WebView
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.web.FrostWebViewClient
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.SingleSubject
import org.apache.commons.text.StringEscapeUtils
import java.util.Locale
class JsBuilder {
private val css = StringBuilder()
private val js = StringBuilder()
private var tag: String? = null
fun css(css: String): JsBuilder {
this.css.append(StringEscapeUtils.escapeEcmaScript(css))
return this
}
fun js(content: String): JsBuilder {
this.js.append(content)
return this
}
fun single(tag: String): JsBuilder {
this.tag = "_frost_${tag.toLowerCase(Locale.CANADA)}"
return this
}
fun build() = JsInjector(toString())
override fun toString(): String {
val tag = this.tag
val builder = StringBuilder().apply {
append("!function(){")
if (css.isNotBlank()) {
val cssMin = css.replace(Regex("\\s*\n\\s*"), "")
append("var a=document.createElement('style');")
append("a.innerHTML='$cssMin';")
if (tag != null) append("a.id='$tag';")
append("document.head.appendChild(a);")
}
if (js.isNotBlank())
append(js)
}
var content = builder.append("}()").toString()
if (tag != null) content = singleInjector(tag, content)
return content
}
private fun singleInjector(tag: String, content: String) = StringBuilder().apply {
append("if (!window.hasOwnProperty(\"$tag\")) {")
append("console.log(\"Registering $tag\");")
append("window.$tag = true;")
append(content)
append("}")
}.toString()
}
/**
* Contract for all injectors to allow it to interact properly with a webview
*/
interface InjectorContract {
fun inject(webView: WebView) = inject(webView, null)
fun inject(webView: WebView, callback: (() -> Unit)?)
/**
* Toggle the injector (usually through Prefs
* If false, will fallback to an empty action
*/
fun maybe(enable: Boolean): InjectorContract = if (enable) this else JsActions.EMPTY
}
/**
* Helper method to inject multiple functions simultaneously with a single callback
*/
fun WebView.jsInject(vararg injectors: InjectorContract, callback: ((Int) -> Unit)? = null): Disposable? {
val validInjectors = injectors.filter { it != JsActions.EMPTY }
if (validInjectors.isEmpty()) {
callback?.invoke(0)
return null
}
L.d { "Injecting ${validInjectors.size} items" }
if (callback == null) {
validInjectors.forEach { it.inject(this) }
return null
}
val observables = Array(validInjectors.size) { SingleSubject.create() }
val disposable = Single.zip(observables.asList()) { it.size }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { res, _ ->
callback(res)
}
(0 until validInjectors.size).forEach { i ->
validInjectors[i].inject(this) {
observables[i].onSuccess(Unit)
}
}
return disposable
}
fun FrostWebViewClient.jsInject(
vararg injectors: InjectorContract,
callback: ((Int) -> Unit)? = null
) = web.jsInject(*injectors, callback = callback)
/**
* Wrapper class to convert a function into an injector
*/
class JsInjector(val function: String) : InjectorContract {
override fun inject(webView: WebView, callback: (() -> Unit)?) {
webView.evaluateJavascript(function) { callback?.invoke() }
}
}