aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-05-29 15:05:26 -0700
committerAllan Wang <me@allanwang.ca>2017-05-29 15:05:26 -0700
commite33120d454aeb72acc2c1315bddea31dd2f15bb2 (patch)
tree6754e8c5c9e8303b0943d1716560b31a0953361a /app/src/main/kotlin
parentf83fb56dca63a06e4706e6cb404bca9c49e5dbe0 (diff)
downloadfrost-e33120d454aeb72acc2c1315bddea31dd2f15bb2.tar.gz
frost-e33120d454aeb72acc2c1315bddea31dd2f15bb2.tar.bz2
frost-e33120d454aeb72acc2c1315bddea31dd2f15bb2.zip
initial port
Diffstat (limited to 'app/src/main/kotlin')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt19
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt127
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt17
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FBURL.kt15
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt75
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Changelog.kt98
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/FragmentUtils.kt15
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Kotterknife.kt137
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt12
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt30
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt11
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt138
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/SwipeRefreshBase.kt31
13 files changed, 725 insertions, 0 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
new file mode 100644
index 00000000..4e287ee7
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
@@ -0,0 +1,19 @@
+package com.pitchedapps.frost
+
+import android.app.Application
+import com.pitchedapps.frost.utils.Prefs
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+class FrostApp : Application() {
+
+ companion object {
+ lateinit var prefs: Prefs
+ }
+
+ override fun onCreate() {
+ prefs = Prefs(applicationContext)
+ super.onCreate()
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt
new file mode 100644
index 00000000..4922a292
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt
@@ -0,0 +1,127 @@
+package com.pitchedapps.frost
+
+import android.os.Bundle
+import android.support.design.widget.FloatingActionButton
+import android.support.design.widget.Snackbar
+import android.support.design.widget.TabLayout
+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.app.AppCompatActivity
+import android.support.v7.widget.Toolbar
+import android.view.*
+import android.widget.TextView
+import butterknife.ButterKnife
+import com.pitchedapps.frost.fragments.WebFragment
+import com.pitchedapps.frost.utils.Changelog
+import com.pitchedapps.frost.utils.bindView
+
+class MainActivity : AppCompatActivity() {
+
+ private var mSectionsPagerAdapter: SectionsPagerAdapter? = null
+ val toolbar: Toolbar by bindView(R.id.toolbar)
+ val viewPager: ViewPager by bindView(R.id.container)
+ val fab: FloatingActionButton by bindView(R.id.fab)
+ val tabs: TabLayout by bindView(R.id.tabs)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ ButterKnife.bind(this)
+ setSupportActionBar(toolbar)
+ // Create the adapter that will return a fragment for each of the three
+ // primary sections of the activity.
+ mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager)
+
+ // Set up the ViewPager with the sections adapter.
+ viewPager.adapter = mSectionsPagerAdapter
+ viewPager.offscreenPageLimit = 5
+ tabs.setupWithViewPager(viewPager)
+
+ fab.setOnClickListener { view ->
+ Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
+ .setAction("Action", null).show()
+ }
+
+ }
+
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ when (item.itemId) {
+ R.id.action_settings -> return true
+ R.id.action_changelog -> Changelog.show(this)
+ else -> return super.onOptionsItemSelected(item)
+ }
+ return true
+ }
+
+ /**
+ * A placeholder fragment containing a simple view.
+ */
+ class PlaceholderFragment : Fragment() {
+
+ override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
+ (rootView.findViewById(R.id.section_label) as TextView).text = getString(R.string.section_format, arguments.getInt(ARG_SECTION_NUMBER))
+ return rootView
+ }
+
+ companion object {
+ /**
+ * The fragment argument representing the section number for this
+ * fragment.
+ */
+ private val ARG_SECTION_NUMBER = "section_number"
+
+ /**
+ * Returns a new instance of this fragment for the given section
+ * number.
+ */
+ fun newInstance(sectionNumber: Int): PlaceholderFragment {
+ val fragment = PlaceholderFragment()
+ val args = Bundle()
+ args.putInt(ARG_SECTION_NUMBER, sectionNumber)
+ fragment.arguments = args
+ return fragment
+ }
+ }
+ }
+
+ /**
+ * A [FragmentPagerAdapter] that returns a fragment corresponding to
+ * one of the sections/tabs/pages.
+ */
+ inner class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
+
+ override fun getItem(position: Int): Fragment {
+ // getItem is called to instantiate the fragment for the given page.
+ // Return a PlaceholderFragment (defined as a static inner class below).
+ return WebFragment.newInstance()
+ }
+
+ override fun getCount(): Int {
+ // Show 3 total pages.
+ return 3
+ }
+
+ override fun getPageTitle(position: Int): CharSequence? {
+ when (position) {
+ 0 -> return "SECTION 1"
+ 1 -> return "SECTION 2"
+ 2 -> return "SECTION 3"
+ }
+ return null
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
new file mode 100644
index 00000000..e8c65be3
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
@@ -0,0 +1,17 @@
+package com.pitchedapps.frost
+
+import android.content.Intent
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+class StartActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ startActivity(Intent(this, MainActivity::class.java))
+ finish()
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FBURL.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FBURL.kt
new file mode 100644
index 00000000..489f12a7
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FBURL.kt
@@ -0,0 +1,15 @@
+package com.pitchedapps.frost.facebook
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ */
+enum class FBURL(val url: String) {
+ FEED("https://touch.facebook.com/"),
+ PROFILE("https://touch.facebook.com/me/"),
+ BOOKMARKS("https://touch.facebook.com/bookmarks"),
+ SEARCH("https://touch.facebook.com/search"),
+ EVENTS("https://touch.facebook.com/events/upcoming"),
+ FRIEND_REQUESTS("https://touch.facebook.com/requests"),
+ MESSAGES("https://touch.facebook.com/messages"),
+ NOTIFICATIONS("https://touch.facebook.com/notifications");
+} \ 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
new file mode 100644
index 00000000..140d8df1
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt
@@ -0,0 +1,75 @@
+package com.pitchedapps.frost.fragments
+
+import android.os.Bundle
+import android.support.v4.app.Fragment
+import android.support.v4.widget.SwipeRefreshLayout
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import butterknife.ButterKnife
+import butterknife.Unbinder
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.facebook.FBURL
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.bindView
+import com.pitchedapps.frost.utils.withBundle
+import com.pitchedapps.frost.views.FrostWebView
+import com.pitchedapps.frost.views.SwipeRefreshBase
+import com.pitchedapps.frost.views.WebStatus
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ */
+
+
+class WebFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
+
+ override fun onRefresh() {
+ web.reload()
+ }
+
+ companion object {
+ private val ARG_URL = "arg_url"
+ fun newInstance(url: String) = WebFragment().withBundle { b -> b.putString(ARG_URL, url) }
+ fun newInstance(url: FBURL = FBURL.FEED) = newInstance(url.url)
+ }
+
+ val refresh: SwipeRefreshBase by bindView(R.id.swipe_refresh)
+ val web: FrostWebView by bindView(R.id.frost_webview)
+ lateinit var url: String
+ private lateinit var unbinder: Unbinder
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ url = arguments.getString(ARG_URL)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ super.onCreateView(inflater, container, savedInstanceState)
+ val view = inflater.inflate(R.layout.swipe_webview, container, false)
+ unbinder = ButterKnife.bind(this, view)
+ return view
+ }
+
+ override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ web.observable.subscribe {
+ t: WebStatus ->
+ when (t) {
+ WebStatus.LOADED, WebStatus.ERROR -> refresh.isRefreshing = false
+ WebStatus.LOADING -> refresh.isRefreshing = true
+ }
+ }
+ refresh.setOnRefreshListener(this)
+ refresh.shouldSwipe = {
+ L.e("Y ${web.scrollY}")
+ SwipeRefreshBase.shouldScroll(web)
+ }
+ web.loadUrl(url)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ unbinder.unbind()
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Changelog.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Changelog.kt
new file mode 100644
index 00000000..14a095a1
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Changelog.kt
@@ -0,0 +1,98 @@
+package com.pitchedapps.frost.utils
+
+import android.content.Context
+import android.content.res.XmlResourceParser
+import android.os.Handler
+import android.support.annotation.LayoutRes
+import android.support.annotation.NonNull
+import android.support.annotation.XmlRes
+import android.support.v4.app.FragmentActivity
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.afollestad.materialdialogs.MaterialDialog
+import com.pitchedapps.frost.R
+import org.xmlpull.v1.XmlPullParser
+import java.util.*
+
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+class Changelog {
+ companion object {
+ fun show(@NonNull activity: FragmentActivity, @XmlRes xmlRes: Int = R.xml.changelog) {
+ val mHandler = Handler()
+ Thread(Runnable {
+ val items = parse(activity, xmlRes)
+ mHandler.post(object : TimerTask() {
+ override fun run() {
+ MaterialDialog.Builder(activity)
+ .title(R.string.changelog)
+ .positiveText(R.string.great)
+ .adapter(ChangelogAdapter(items), null)
+ .show()
+ }
+ })
+ }).start()
+ }
+ }
+}
+
+private class ChangelogAdapter(val items: List<Pair<String, ChangelogType>>) : RecyclerView.Adapter<ChangelogAdapter.ChangelogVH>() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ChangelogVH(LayoutInflater.from(parent.context)
+ .inflate(getLayout(viewType), parent, false))
+
+ private fun getLayout(position: Int) = items[position].second.layout
+
+ override fun onBindViewHolder(holder: ChangelogVH, position: Int) {
+ holder.text.text = items[position].first
+ }
+
+ override fun getItemId(position: Int) = position.toLong()
+
+ override fun getItemViewType(position: Int) = position
+
+ override fun getItemCount() = items.size
+
+ internal class ChangelogVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val text: TextView = itemView.findViewById(R.id.changelog_text) as TextView
+ }
+}
+
+private fun parse(context: Context, @XmlRes xmlRes: Int): List<Pair<String, ChangelogType>> {
+ val items = mutableListOf<Pair<String, ChangelogType>>()
+ context.resources.getXml(xmlRes).use {
+ parser ->
+ var eventType = parser.eventType
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG)
+ ChangelogType.values.any { it.add(parser, items) }
+ eventType = parser.next()
+ }
+ }
+ return items
+}
+
+private enum class ChangelogType(val tag: String, val attr: String, @LayoutRes val layout: Int) {
+ TITLE("title", "version", R.layout.changelog_title),
+ ITEM("item", "text", R.layout.changelog_content);
+
+ companion object {
+ val values = values()
+ }
+
+ /**
+ * Returns true if tag matches; false otherwise
+ */
+ fun add(parser: XmlResourceParser, list: MutableList<Pair<String, ChangelogType>>): Boolean {
+ if (parser.name != tag) return false
+ if (parser.getAttributeValue(null, attr).isNotBlank())
+ list.add(Pair(parser.getAttributeValue(null, attr), this))
+ return true
+ }
+}
+
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/FragmentUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/FragmentUtils.kt
new file mode 100644
index 00000000..cd638068
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/FragmentUtils.kt
@@ -0,0 +1,15 @@
+package com.pitchedapps.frost.utils
+
+import android.os.Bundle
+import android.support.v4.app.Fragment
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ */
+
+fun Fragment.withBundle(creator: (Bundle) -> Unit): Fragment {
+ val bundle = Bundle()
+ creator.invoke(bundle)
+ this.arguments = bundle
+ return this
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Kotterknife.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Kotterknife.kt
new file mode 100644
index 00000000..6e3b5c24
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Kotterknife.kt
@@ -0,0 +1,137 @@
+package com.pitchedapps.frost.utils
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ *
+ * Courtesy of Jake Wharton
+ *
+ * https://github.com/JakeWharton/kotterknife/blob/master/src/main/kotlin/kotterknife/ButterKnife.kt
+ */
+import android.app.Activity
+import android.app.Dialog
+import android.app.DialogFragment
+import android.app.Fragment
+import android.support.v7.widget.RecyclerView.ViewHolder
+import android.view.View
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+import android.support.v4.app.DialogFragment as SupportDialogFragment
+import android.support.v4.app.Fragment as SupportFragment
+
+public fun <V : View> View.bindView(id: Int)
+ : ReadOnlyProperty<View, V> = required(id, viewFinder)
+public fun <V : View> Activity.bindView(id: Int)
+ : ReadOnlyProperty<Activity, V> = required(id, viewFinder)
+public fun <V : View> Dialog.bindView(id: Int)
+ : ReadOnlyProperty<Dialog, V> = required(id, viewFinder)
+public fun <V : View> DialogFragment.bindView(id: Int)
+ : ReadOnlyProperty<DialogFragment, V> = required(id, viewFinder)
+public fun <V : View> SupportDialogFragment.bindView(id: Int)
+ : ReadOnlyProperty<SupportDialogFragment, V> = required(id, viewFinder)
+public fun <V : View> Fragment.bindView(id: Int)
+ : ReadOnlyProperty<Fragment, V> = required(id, viewFinder)
+public fun <V : View> SupportFragment.bindView(id: Int)
+ : ReadOnlyProperty<SupportFragment, V> = required(id, viewFinder)
+public fun <V : View> ViewHolder.bindView(id: Int)
+ : ReadOnlyProperty<ViewHolder, V> = required(id, viewFinder)
+
+public fun <V : View> View.bindOptionalView(id: Int)
+ : ReadOnlyProperty<View, V?> = optional(id, viewFinder)
+public fun <V : View> Activity.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Activity, V?> = optional(id, viewFinder)
+public fun <V : View> Dialog.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Dialog, V?> = optional(id, viewFinder)
+public fun <V : View> DialogFragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<DialogFragment, V?> = optional(id, viewFinder)
+public fun <V : View> SupportDialogFragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<SupportDialogFragment, V?> = optional(id, viewFinder)
+public fun <V : View> Fragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Fragment, V?> = optional(id, viewFinder)
+public fun <V : View> SupportFragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<SupportFragment, V?> = optional(id, viewFinder)
+public fun <V : View> ViewHolder.bindOptionalView(id: Int)
+ : ReadOnlyProperty<ViewHolder, V?> = optional(id, viewFinder)
+
+public fun <V : View> View.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<View, List<V>> = required(ids, viewFinder)
+public fun <V : View> Activity.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Activity, List<V>> = required(ids, viewFinder)
+public fun <V : View> Dialog.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Dialog, List<V>> = required(ids, viewFinder)
+public fun <V : View> DialogFragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<DialogFragment, List<V>> = required(ids, viewFinder)
+public fun <V : View> SupportDialogFragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<SupportDialogFragment, List<V>> = required(ids, viewFinder)
+public fun <V : View> Fragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Fragment, List<V>> = required(ids, viewFinder)
+public fun <V : View> SupportFragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<SupportFragment, List<V>> = required(ids, viewFinder)
+public fun <V : View> ViewHolder.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<ViewHolder, List<V>> = required(ids, viewFinder)
+
+public fun <V : View> View.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<View, List<V>> = optional(ids, viewFinder)
+public fun <V : View> Activity.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Activity, List<V>> = optional(ids, viewFinder)
+public fun <V : View> Dialog.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Dialog, List<V>> = optional(ids, viewFinder)
+public fun <V : View> DialogFragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<DialogFragment, List<V>> = optional(ids, viewFinder)
+public fun <V : View> SupportDialogFragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<SupportDialogFragment, List<V>> = optional(ids, viewFinder)
+public fun <V : View> Fragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Fragment, List<V>> = optional(ids, viewFinder)
+public fun <V : View> SupportFragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<SupportFragment, List<V>> = optional(ids, viewFinder)
+public fun <V : View> ViewHolder.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<ViewHolder, List<V>> = optional(ids, viewFinder)
+
+private val View.viewFinder: View.(Int) -> View?
+ get() = { findViewById(it) }
+private val Activity.viewFinder: Activity.(Int) -> View?
+ get() = { findViewById(it) }
+private val Dialog.viewFinder: Dialog.(Int) -> View?
+ get() = { findViewById(it) }
+private val DialogFragment.viewFinder: DialogFragment.(Int) -> View?
+ get() = { dialog.findViewById(it) }
+private val SupportDialogFragment.viewFinder: SupportDialogFragment.(Int) -> View?
+ get() = { dialog.findViewById(it) }
+private val Fragment.viewFinder: Fragment.(Int) -> View?
+ get() = { view.findViewById(it) }
+private val SupportFragment.viewFinder: SupportFragment.(Int) -> View?
+ get() = { view!!.findViewById(it) }
+private val ViewHolder.viewFinder: ViewHolder.(Int) -> View?
+ get() = { itemView.findViewById(it) }
+
+private fun viewNotFound(id:Int, desc: KProperty<*>): Nothing =
+ throw IllegalStateException("View ID $id for '${desc.name}' not found.")
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> required(id: Int, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> optional(id: Int, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> t.finder(id) as V? }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> required(ids: IntArray, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> ids.map { t.finder(it) as V? ?: viewNotFound(it, desc) } }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> optional(ids: IntArray, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> ids.map { t.finder(it) as V? }.filterNotNull() }
+
+// Like Kotlin's lazy delegate but the initializer gets the target and metadata passed to it
+private class Lazy<T, V>(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty<T, V> {
+ private object EMPTY
+ private var value: Any? = EMPTY
+
+ override fun getValue(thisRef: T, property: KProperty<*>): V {
+ if (value == EMPTY) {
+ value = initializer(thisRef, property)
+ }
+ @Suppress("UNCHECKED_CAST")
+ return value as V
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt
new file mode 100644
index 00000000..279d595e
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt
@@ -0,0 +1,12 @@
+package com.pitchedapps.frost.utils
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+class L {
+ companion object {
+ val TAG = "Frost"
+ fun e(s: String) = android.util.Log.e(com.pitchedapps.frost.utils.L.Companion.TAG, s)
+ fun d(s: String) = android.util.Log.d(com.pitchedapps.frost.utils.L.Companion.TAG, s)
+ }
+} \ 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
new file mode 100644
index 00000000..c98fd5a5
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
@@ -0,0 +1,30 @@
+package com.pitchedapps.frost.utils
+
+import com.pitchedapps.frost.FrostApp
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+val prefs: Prefs by lazy { FrostApp.prefs }
+
+class Prefs(c: android.content.Context) {
+ private companion object {
+ val PREFERENCE_NAME = "${com.pitchedapps.frost.BuildConfig.APPLICATION_ID}.prefs"
+ val LAST_ACTIVE = "last_active"
+ }
+
+ var lastActive: Long
+ get() = prefs.getLong(com.pitchedapps.frost.utils.Prefs.Companion.LAST_ACTIVE, -1)
+ set(value) = set(com.pitchedapps.frost.utils.Prefs.Companion.LAST_ACTIVE, System.currentTimeMillis())
+
+ init {
+ lastActive = 0
+ }
+
+ private val prefs: android.content.SharedPreferences by lazy { c.getSharedPreferences(com.pitchedapps.frost.utils.Prefs.Companion.PREFERENCE_NAME, android.content.Context.MODE_PRIVATE) }
+
+ private fun set(key: String, value: Boolean) = prefs.edit().putBoolean(key, value).apply()
+ private fun set(key: String, value: Int) = prefs.edit().putInt(key, value).apply()
+ private fun set(key: String, value: Long) = prefs.edit().putLong(key, value).apply()
+ private fun set(key: String, value: String) = prefs.edit().putString(key, value).apply()
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
new file mode 100644
index 00000000..bbf0e1f0
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
@@ -0,0 +1,11 @@
+package com.pitchedapps.frost.utils
+
+import android.content.res.Resources
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+object Utils {
+ fun dpToPx(dp: Int) = (dp * android.content.res.Resources.getSystem().displayMetrics.density).toInt()
+ fun pxToDp(px:Int) = (px / android.content.res.Resources.getSystem().displayMetrics.density).toInt()
+} \ 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..e7314c20
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
@@ -0,0 +1,138 @@
+package com.pitchedapps.frost.views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.support.v4.view.MotionEventCompat
+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.View
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import com.pitchedapps.frost.utils.L
+import io.reactivex.subjects.BehaviorSubject
+import io.reactivex.subjects.Subject
+
+enum class WebStatus {
+ LOADING, LOADED, ERROR
+}
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ *
+ * 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
+ */
+class FrostWebView @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 observable: Subject<WebStatus>
+
+ init {
+ isNestedScrollingEnabled = true
+ observable = BehaviorSubject.create<WebStatus>()
+ setupWebview()
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ fun setupWebview() {
+ settings.javaScriptEnabled = true
+ setLayerType(View.LAYER_TYPE_HARDWARE, null)
+ setWebViewClient(object : WebViewClient() {
+ override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
+ super.onReceivedError(view, request, error)
+ observable.onNext(WebStatus.ERROR)
+ L.e("Error ${request}")
+ }
+
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ super.onPageStarted(view, url, favicon)
+ observable.onNext(WebStatus.LOADING)
+ }
+
+ override fun onPageFinished(view: WebView?, url: String?) {
+ super.onPageFinished(view, url)
+ observable.onNext(WebStatus.LOADED)
+// CookieManager.getInstance().flush()
+ }
+ })
+ }
+
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
+ val event = MotionEvent.obtain(ev)
+ val action = MotionEventCompat.getActionMasked(event)
+ 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 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/views/SwipeRefreshBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/SwipeRefreshBase.kt
new file mode 100644
index 00000000..59d42722
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/SwipeRefreshBase.kt
@@ -0,0 +1,31 @@
+package com.pitchedapps.frost.views
+
+import android.content.Context
+import android.support.v4.widget.SwipeRefreshLayout
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.webkit.WebView
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.Utils
+
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+class SwipeRefreshBase @JvmOverloads constructor(
+ context: Context?, attrs: AttributeSet? = null
+) : SwipeRefreshLayout(context, attrs) {
+
+ lateinit var shouldSwipe: (ev: MotionEvent) -> Boolean
+
+ companion object {
+ private val SCROLL_BUFFER by lazy { Utils.dpToPx(5) }
+ fun shouldScroll(webview: WebView) = webview.scrollY <= SCROLL_BUFFER
+ }
+
+// override fun onInterceptTouchEvent(ev: MotionEvent):Boolean {
+// val b = shouldSwipe.invoke(ev) && super.onInterceptTouchEvent(ev)
+// L.e("Should swipe $b")
+// return b
+// }
+} \ No newline at end of file