diff --git a/.gitignore b/.gitignore
index 520a86352..39fb081a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,55 +1,9 @@
-# Built application files
-*.apk
-*.ap_
-
-# Files for the ART/Dalvik VM
-*.dex
-
-# Java class files
-*.class
-
-# Generated files
-bin/
-gen/
-out/
-
-# Gradle files
-.gradle/
-build/
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Proguard folder generated by Eclipse
-proguard/
-
-# Log Files
-*.log
-
-# Android Studio Navigation editor temp files
-.navigation/
-
-# Android Studio captures folder
-captures/
-
-# Intellij
*.iml
-.idea/workspace.xml
-.idea/tasks.xml
-.idea/gradle.xml
-.idea/dictionaries
-.idea/libraries
-
-# Keystore files
-*.jks
-
-# External native build folder generated in Android Studio 2.2 and later
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
.externalNativeBuild
-
-# Google Services (e.g. APIs or Firebase)
-google-services.json
-
-# Freeline
-freeline.py
-freeline/
-freeline_project_description.json
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 000000000..96cc43efa
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 000000000..e7bedf337
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 000000000..97626ba45
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 000000000..7ac24c777
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml
new file mode 100644
index 000000000..838ef3987
--- /dev/null
+++ b/.idea/markdown-navigator.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml
new file mode 100644
index 000000000..57927c5a7
--- /dev/null
+++ b/.idea/markdown-navigator/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..085136f8d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Android
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.8
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 000000000..7451d7198
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 000000000..7f68460d8
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..94a25f7f4
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 000000000..ab9c27708
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,76 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'realm-android'
+
+android {
+ compileSdkVersion Integer.parseInt(project.TARGET_SDK)
+ buildToolsVersion project.BUILD_TOOLS
+
+ defaultConfig {
+ applicationId "${project.APP_GROUP}." + project.APP_ID.toLowerCase() + ".sample"
+ minSdkVersion Integer.parseInt(project.MIN_SDK)
+ targetSdkVersion Integer.parseInt(project.TARGET_SDK)
+ versionCode Integer.parseInt(project.VERSION_CODE)
+ versionName project.VERSION_NAME
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ applicationVariants.all { variant ->
+ variant.outputs.each { output ->
+ output.outputFile = new File(output.outputFile.parent,
+ "${project.APP_ID}-v${variant.versionName}.apk")
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ testCompile 'junit:junit:4.12'
+
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
+
+ compile "com.android.support:appcompat-v7:${ANDROID_SUPPORT_LIBS}"
+ compile "com.android.support:support-v4:${ANDROID_SUPPORT_LIBS}"
+ compile "com.android.support:support-v13:${ANDROID_SUPPORT_LIBS}"
+ compile "com.android.support:design:${ANDROID_SUPPORT_LIBS}"
+ compile "com.android.support:recyclerview-v7:${ANDROID_SUPPORT_LIBS}"
+ compile "com.android.support:cardview-v7:${ANDROID_SUPPORT_LIBS}"
+ compile "com.android.support:preference-v14:${ANDROID_SUPPORT_LIBS}"
+
+ //Logging
+ compile "com.jakewharton.timber:timber:${TIMBER}"
+
+ //Dialog
+ compile "com.afollestad.material-dialogs:core:${MD}"
+
+ //Icons
+ compile "com.mikepenz:iconics-core:${ICONICS}@aar"
+ compile "com.mikepenz:google-material-typeface:${GMD}.original@aar"
+
+ //Butterknife
+ compile "com.jakewharton:butterknife:${BUTTERKNIFE}"
+ annotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE}"
+
+ compile "io.reactivex.rxjava2:rxjava:${RX_JAVA}"
+ compile "io.reactivex.rxjava2:rxandroid:${RX_ANDROID}"
+ compile "com.jakewharton.rxbinding2:rxbinding:${RX_BINDING}"
+ compile "com.jakewharton.rxbinding2:rxbinding-appcompat-v7:${RX_BINDING}"
+
+ compile "com.facebook.stetho:stetho-okhttp3:${STETHO}"
+
+ compile "com.github.bumptech.glide:glide:${GLIDE}"
+ annotationProcessor "com.github.bumptech.glide:compiler:${GLIDE}"
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 000000000..2b766bc42
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:\Users\User7681\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/androidTest/java/com/pitchedapps/frost/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/pitchedapps/frost/ExampleInstrumentedTest.java
new file mode 100644
index 000000000..9e27deafa
--- /dev/null
+++ b/app/src/androidTest/java/com/pitchedapps/frost/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.pitchedapps.frost;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.pitchedapps.frost", appContext.getPackageName());
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..744285033
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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 000000000..4e287ee7b
--- /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 000000000..4922a2924
--- /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 000000000..e8c65be3b
--- /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 000000000..489f12a7f
--- /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 000000000..140d8df15
--- /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 000000000..14a095a17
--- /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>) : RecyclerView.Adapter() {
+
+ 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> {
+ val items = mutableListOf>()
+ 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>): 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 000000000..cd638068a
--- /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 000000000..6e3b5c24c
--- /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 View.bindView(id: Int)
+ : ReadOnlyProperty = required(id, viewFinder)
+public fun Activity.bindView(id: Int)
+ : ReadOnlyProperty = required(id, viewFinder)
+public fun Dialog.bindView(id: Int)
+ : ReadOnlyProperty