Bimba.git

commit 0954a8c255b6f457b44953891d63dce6704d53e4

Author: Adam Pioterek <adam.pioterek@protonmail.ch>

favourites, no refresh when searchbar has focus

 .gitignore | 1 
 app/build.gradle | 4 
 app/src/main/AndroidManifest.xml | 2 
  | 80 
 app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt | 2 
 app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt | 2 
 app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt | 25 
 app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt | 4 
 app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt | 40 
 app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt | 75 
 app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt | 11 
 app/src/main/res/drawable/ic_favourite_empty.xml | 9 
 app/src/main/res/drawable/ic_more.xml | 9 
  | 2 
 app/src/main/res/layout/activity_stop.xml | 1 
  | 0 
 app/src/main/res/layout/row_favourite.xml | 64 
 app/src/main/res/menu/favourite_actions.xml | 11 
 app/src/main/res/values-pl/strings.xml | 3 
 app/src/main/res/values/strings.xml | 4 
 research/scraper.py | 251 


diff --git a/.gitignore b/.gitignore
index 25e5aea018ddc954e9616352ba97ad36fcdb9ab8..03e31c52b899a1f7d7fae329bddd1c922968b798 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,5 +49,6 @@ research/cron
 research/peka-vm-api.md
 research/svg/
 research/timetable/
+research/timetable_new/
 research/timetable.db
 research/timetable.db-journal




diff --git a/app/build.gradle b/app/build.gradle
index 6f0fc81f446064077f1787e2f4ce4a60915f4042..3af2e3522988b21a6b14c51fb7d4a649747eeb19 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -8,8 +8,8 @@     defaultConfig {
         applicationId "ml.adamsprogs.bimba"
         minSdkVersion 19
         targetSdkVersion 25
-        versionCode 2
-        versionName "1.0.1"
+        versionCode 3
+        versionName "1.1"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
         vectorDrawables.useSupportLibrary = true
     }




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d39599789d29518ca69c703776b72dba55903f21..907ec3e2fdfe60bc23b6a159134f22cc61a30d06 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,7 +11,7 @@         android:icon="@drawable/logo"
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
-        <activity android:name=".activities.MainActivity" />
+        <activity android:name=".activities.DashActivity" />
 
         <service
             android:name=".TimetableDownloader"




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5e7e894d5d329e49b287b8c16c8a34790b64e7aa
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -0,0 +1,203 @@
+package ml.adamsprogs.bimba.activities
+
+import android.content.*
+import android.os.*
+import android.support.design.widget.Snackbar
+import android.support.v7.app.*
+import android.text.Html
+import android.view.View
+import com.arlib.floatingsearchview.FloatingSearchView
+import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
+import ml.adamsprogs.bimba.models.*
+import kotlin.concurrent.thread
+import android.app.Activity
+import android.support.v4.widget.*
+import android.support.v7.widget.*
+import android.util.Log
+import android.view.inputmethod.InputMethodManager
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import ml.adamsprogs.bimba.*
+
+//todo refresh every 15s
+class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, SwipeRefreshLayout.OnRefreshListener {
+    val context: Context = this
+    val receiver = MessageReceiver()
+    lateinit var timetable: Timetable
+    var stops: ArrayList<StopSuggestion>? = null
+    lateinit var swipeRefreshLayout: SwipeRefreshLayout
+    lateinit var favouritesList: RecyclerView
+    lateinit var searchView: FloatingSearchView
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_dash)
+        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO)
+
+        prepareSwipeLayout()
+
+        prepareOnDownloadListener()
+        startDownloaderService()
+
+        getStops()
+
+        prepareFavourites()
+
+        searchView = findViewById(R.id.search_view) as FloatingSearchView
+
+        searchView.setOnFocusChangeListener(object : FloatingSearchView.OnFocusChangeListener {
+            override fun onFocus() {
+                swipeRefreshLayout.isEnabled = false
+                favouritesList.visibility = View.GONE
+            }
+
+            override fun onFocusCleared() {
+                swipeRefreshLayout.isEnabled = true
+                favouritesList.visibility = View.VISIBLE
+            }
+        })
+
+        searchView.setOnQueryChangeListener({ oldQuery, newQuery ->
+            if (oldQuery != "" && newQuery == "")
+                searchView.clearSuggestions()
+            thread {
+                val newStops = stops!!.filter { deAccent(it.body.split("\n")[0]).contains(deAccent(newQuery), true) }
+                runOnUiThread { searchView.swapSuggestions(newStops) }
+            }
+        })
+
+        searchView.setOnSearchListener(object : FloatingSearchView.OnSearchListener {
+            override fun onSuggestionClicked(searchSuggestion: SearchSuggestion) {
+                val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
+                var view = (context as DashActivity).currentFocus
+                if (view == null) {
+                    view = View(context)
+                }
+                imm.hideSoftInputFromWindow(view.windowToken, 0)
+                intent = Intent(context, StopActivity::class.java)
+                intent.putExtra("stopId", (searchSuggestion as StopSuggestion).id)
+                intent.putExtra("stopSymbol", (searchSuggestion as StopSuggestion).symbol)
+                startActivity(intent)
+            }
+
+            override fun onSearchAction(query: String) {
+            }
+        })
+
+        searchView.setOnBindSuggestionCallback { _, _, textView, item, _ ->
+            val suggestion = item as StopSuggestion
+            val text = suggestion.body.split("\n")
+            val t = "<small><font color=\"#a0a0a0\">" + text[1] + "</font></small>"
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                textView.text = Html.fromHtml(text[0] + "<br/>" + t, Html.FROM_HTML_MODE_LEGACY)
+            } else {
+                @Suppress("DEPRECATION")
+                textView.text = Html.fromHtml(text[0] + "<br/>" + t)
+            }
+        }
+
+        //todo searchView.attachNavigationDrawerToMenuButton(mDrawerLayout)
+    }
+
+    private fun prepareFavourites() {
+        val layoutManager = LinearLayoutManager(context)
+        favouritesList = findViewById(R.id.favouritesList) as RecyclerView
+        favouritesList.adapter = FavouritesAdapter(context, getFavourites())
+        favouritesList.layoutManager = layoutManager
+    }
+
+    private fun getFavourites(): ArrayList<Favourite> {
+        val preferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)
+        val favouritesString = preferences.getString("favourites", "{}")
+        val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java)
+        val favourites = ArrayList<Favourite>()
+        for ((name, jsonTimetables) in favouritesMap.entrySet()) {
+            val timetables = ArrayList<HashMap<String, String>>()
+            for (jsonTimetable in jsonTimetables.asJsonArray) {
+                val timetable = HashMap<String, String>()
+                timetable["stop"] = jsonTimetable.asJsonObject["stop"].asString
+                timetable["line"] = jsonTimetable.asJsonObject["line"].asString
+                timetables.add(timetable)
+            }
+            favourites.add(Favourite(name, timetables, context))
+        }
+        return favourites
+    }
+
+    private fun getStops() {
+        timetable = Timetable(this)
+        stops = timetable.getStops()
+    }
+
+    private fun prepareOnDownloadListener() {
+        val filter = IntentFilter("ml.adamsprogs.bimba.timetableDownloaded")
+        filter.addCategory(Intent.CATEGORY_DEFAULT)
+        registerReceiver(receiver, filter)
+        receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
+    }
+
+    private fun startDownloaderService() {
+        startService(Intent(context, TimetableDownloader::class.java))
+    }
+
+    private fun prepareSwipeLayout() {
+        swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout) as SwipeRefreshLayout
+        swipeRefreshLayout.isEnabled = true
+        swipeRefreshLayout.setOnRefreshListener(this)
+        swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary)
+    }
+
+    override fun onRefresh() {
+        swipeRefreshLayout.isRefreshing = true
+        Log.i("Refresh", "Downloading")
+        startDownloaderService()
+    }
+
+    override fun onBackPressed() {
+        if (!searchView.setSearchFocused(false)) {
+            super.onBackPressed()
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        favouritesList.adapter = FavouritesAdapter(context, getFavourites())
+        favouritesList.adapter.notifyDataSetChanged()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
+        unregisterReceiver(receiver)
+        timetable.close()
+    }
+
+    fun deAccent(str: String): String {
+        var result = str.replace('ę', 'e')
+        result = result.replace('ó', 'o')
+        result = result.replace('ą', 'a')
+        result = result.replace('ś', 's')
+        result = result.replace('ł', 'l')
+        result = result.replace('ż', 'ż')
+        result = result.replace('ź', 'ź')
+        result = result.replace('ć', 'ć')
+        result = result.replace('ń', 'n')
+        return result
+    }
+
+    override fun onTimetableDownload(result: String?) {
+        Log.i("Refresh", "downloaded: $result")
+        val message: String
+        when (result) {
+            "downloaded" -> message = getString(R.string.timetable_downloaded)
+            "no connectivity" -> message = getString(R.string.no_connectivity)
+            "up-to-date" -> message = getString(R.string.timetable_up_to_date)
+            "validity failed" -> message = getString(R.string.validity_failed)
+            else -> message = getString(R.string.error_try_later)
+        }
+        timetable.refresh()
+        stops = timetable.getStops()
+        Snackbar.make(swipeRefreshLayout, message, Snackbar.LENGTH_LONG).show()
+        swipeRefreshLayout.isRefreshing = false
+    }
+}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/MainActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/MainActivity.kt
deleted file mode 100644
index cbb03117e13aa306d279f6f30ff9310b5cbce8f3..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/MainActivity.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-package ml.adamsprogs.bimba.activities
-
-import android.content.Intent
-import android.content.IntentFilter
-import android.os.*
-import android.support.design.widget.Snackbar
-import android.support.v7.app.*
-import android.text.Html
-import android.view.View
-import com.arlib.floatingsearchview.FloatingSearchView
-import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
-import ml.adamsprogs.bimba.models.*
-import kotlin.concurrent.thread
-import android.app.Activity
-import android.content.Context
-import android.support.v4.widget.SwipeRefreshLayout
-import android.util.Log
-import android.view.inputmethod.InputMethodManager
-import ml.adamsprogs.bimba.MessageReceiver
-import ml.adamsprogs.bimba.R
-import ml.adamsprogs.bimba.TimetableDownloader
-
-
-class MainActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, SwipeRefreshLayout.OnRefreshListener {
-    val context: Context = this
-    val receiver = MessageReceiver()
-    lateinit var timetable: Timetable
-    var stops: ArrayList<StopSuggestion>? = null
-    lateinit var swipeRefreshLayout: SwipeRefreshLayout
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_main)
-        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO)
-
-        prepareSwipeLayout()
-
-        prepareOnDownloadListener()
-        startDownloaderService()
-
-        getStops()
-
-        val searchView = findViewById(R.id.search_view) as FloatingSearchView
-
-        searchView.setOnQueryChangeListener({ _, newQuery ->
-            thread {
-                val newStops = stops!!.filter { deAccent(it.body.split("\n")[0]).contains(deAccent(newQuery), true) }
-                runOnUiThread { searchView.swapSuggestions(newStops) }
-            }
-        })
-
-        searchView.setOnSearchListener(object : FloatingSearchView.OnSearchListener {
-            override fun onSuggestionClicked(searchSuggestion: SearchSuggestion) {
-                val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
-                var view = (context as MainActivity).currentFocus
-                if (view == null) {
-                    view = View(context)
-                }
-                imm.hideSoftInputFromWindow(view.windowToken, 0)
-                intent = Intent(context, StopActivity::class.java)
-                intent.putExtra("stopId", (searchSuggestion as StopSuggestion).id)
-                intent.putExtra("stopSymbol", (searchSuggestion as StopSuggestion).symbol)
-                startActivity(intent)
-            }
-
-            override fun onSearchAction(query: String) {
-            }
-        })
-
-        searchView.setOnBindSuggestionCallback { _, _, textView, item, _ ->
-            val suggestion = item as StopSuggestion
-            val text = suggestion.body.split("\n")
-            val t = "<small><font color=\"#a0a0a0\">" + text[1] + "</font></small>"
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                textView.text = Html.fromHtml(text[0] + "<br/>" + t, Html.FROM_HTML_MODE_LEGACY)
-            } else {
-                @Suppress("DEPRECATION")
-                textView.text = Html.fromHtml(text[0] + "<br/>" + t)
-            }
-        }
-
-        //todo searchView.attachNavigationDrawerToMenuButton(mDrawerLayout)
-    }
-
-    private fun getStops() {
-        timetable = Timetable(this)
-        stops = timetable.getStops()
-    }
-
-    private fun prepareOnDownloadListener() {
-        val filter = IntentFilter("ml.adamsprogs.bimba.timetableDownloaded")
-        filter.addCategory(Intent.CATEGORY_DEFAULT)
-        registerReceiver(receiver, filter)
-        receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
-    }
-
-    private fun startDownloaderService() {
-        startService(Intent(context, TimetableDownloader::class.java))
-    }
-
-    private fun prepareSwipeLayout() {
-        swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout) as SwipeRefreshLayout
-        swipeRefreshLayout.isEnabled = true
-        swipeRefreshLayout.setOnRefreshListener(this)
-        swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary)
-    }
-
-    override fun onRefresh() {
-        swipeRefreshLayout.isRefreshing = true
-        Log.i("Refresh", "Downloading")
-        startDownloaderService()
-    }
-
-    override fun onDestroy() {
-        super.onDestroy()
-        receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
-        unregisterReceiver(receiver)
-        timetable.close()
-    }
-
-    fun deAccent(str: String): String {
-        var result = str.replace('ę', 'e')
-        result = result.replace('ó', 'o')
-        result = result.replace('ą', 'a')
-        result = result.replace('ś', 's')
-        result = result.replace('ł', 'l')
-        result = result.replace('ż', 'ż')
-        result = result.replace('ź', 'ź')
-        result = result.replace('ć', 'ć')
-        result = result.replace('ń', 'n')
-        return result
-    }
-
-    override fun onTimetableDownload(result: String?) {
-        Log.i("Refresh", "downloaded: $result")
-        val message: String
-        when (result) {
-            "downloaded" -> message = getString(R.string.timetable_downloaded)
-            "no connectivity" -> message = getString(R.string.no_connectivity)
-            "up-to-date" -> message = getString(R.string.timetable_up_to_date)
-            "validity failed" -> message = getString(R.string.validity_failed)
-            else -> message = getString(R.string.error_try_later)
-        }
-        timetable.refresh()
-        stops = timetable.getStops()
-        Snackbar.make(swipeRefreshLayout, message, Snackbar.LENGTH_LONG).show()
-        swipeRefreshLayout.isRefreshing = false
-    }
-}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
index 6e89218c7fd22a9565f0f86a86e19f5eac2a622f..01d759ab8d045afdaa50fc9d544e0b5c641e7dea 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
@@ -53,7 +53,7 @@         when (result) {
             "downloaded" -> {
                 timetableDownloadReceiver.removeOnTimetableDownloadListener(this)
                 networkStateReceiver.removeOnConnectivityChangeListener(this)
-                startActivity(Intent(this, MainActivity::class.java))
+                startActivity(Intent(this, DashActivity::class.java))
                 finish()
             }
             else -> (findViewById(R.id.noDbCaption) as TextView).text = getString(R.string.error_try_later)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
index 98a0fed99b714a63fb103c20389d8563598e7847..d1c341f365785ba288f0dcb13ab2f1025de2fb29 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
@@ -12,7 +12,7 @@     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         val timetable = Timetable(this)
         if(timetable.isDatabaseHealthy())
-            startActivity(Intent(this, MainActivity::class.java))
+            startActivity(Intent(this, DashActivity::class.java))
         else
             startActivity(Intent(this, NoDbActivity::class.java))
         timetable.close()




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
index 33cce2f47bec51a56085f45083dc0b8fabe48452..6fc79e3fde4c096a6231ef495ba2caf8543db131 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
@@ -12,7 +12,7 @@ import android.support.v7.widget.*
 import android.support.v4.app.*
 import android.support.v4.view.*
 import android.support.v4.content.res.ResourcesCompat
-import com.google.gson.Gson
+import com.google.gson.*
 
 import ml.adamsprogs.bimba.models.*
 import ml.adamsprogs.bimba.*
@@ -66,15 +66,24 @@
         scheduleRefresh()
 
         val fab = findViewById(R.id.fab) as FloatingActionButton
+
+        var favourites = Gson().fromJson(sharedPreferences.getString("favourites", "{}"), JsonObject::class.java)
+        if (favourites[stopSymbol] == null) {
+            fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite_empty, this.theme))
+        }
+
         fab.setOnClickListener {
-            var favouritesString = sharedPreferences.getString("favourites", "{}")
-            @Suppress("UNCHECKED_CAST")
-            val favourites = Gson().fromJson(favouritesString, HashMap::class.java) as HashMap<String, ArrayList<HashMap<String,String>>>
+            favourites = Gson().fromJson(sharedPreferences.getString("favourites", "{}"), JsonObject::class.java)
             if (favourites[stopSymbol] == null) {
-                val items = ArrayList<HashMap<String, String>>()
-                timetable.getLines(stopId)?.forEach {items.add(mapOf("stop" to stopId, "line" to it) as HashMap<String, String>)}
-                favourites[stopSymbol] = items
-                favouritesString = Gson().toJson(favourites)
+                val items = JsonArray()
+                timetable.getLines(stopId)?.forEach {
+                    val o = JsonObject()
+                    o.addProperty("stop", stopId)
+                    o.addProperty("line", it)
+                    items.add(o)
+                }
+                favourites.add(stopSymbol,items)
+                val favouritesString = Gson().toJson(favourites)
                 val editor = sharedPreferences.edit()
                 editor.putString("favourites", favouritesString)
                 editor.apply()




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
index cf66af128555042a84ca3d9faae926d77b90f692..a8b0c8e50301ad5526416091723a16c271574ed5 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
@@ -96,8 +96,8 @@     override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
         val context = parent?.context
         val inflater = LayoutInflater.from(context)
 
-        val contactView = inflater.inflate(R.layout.departure_row, parent, false)
-        val viewHolder = ViewHolder(contactView)
+        val rowView = inflater.inflate(R.layout.row_departure, parent, false)
+        val viewHolder = ViewHolder(rowView)
         return viewHolder
     }
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7e89bd7184783c96401ef4cc490302041ae0a8d9
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
@@ -0,0 +1,40 @@
+package ml.adamsprogs.bimba.models
+
+import android.content.Context
+import java.util.*
+
+class Favourite(var name: String, var timetables: ArrayList<HashMap<String, String>>, context: Context) {
+    val timetable = Timetable(context)
+
+    var nextDeparture: Departure? = null
+        get() {
+            val today: String
+            val allDepartures = ArrayList<Departure>()
+            val now = Calendar.getInstance()
+            val departureTime = Calendar.getInstance()
+            when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
+                Calendar.SUNDAY -> today = "sundays"
+                Calendar.SATURDAY -> today = "saturdays"
+                else -> today = "workdays"
+            }
+
+            for (t in timetables)
+                allDepartures.addAll(timetable.getStopDepartures(t["stop"] as String, t["line"])!![today]!!)
+            var minDeparture: Departure = allDepartures[0]
+            var minInterval = 24 * 60L
+            for (departure in allDepartures) {
+                departureTime.set(Calendar.HOUR_OF_DAY, Integer.parseInt(departure.time.split(":")[0]))
+                departureTime.set(Calendar.MINUTE, Integer.parseInt(departure.time.split(":")[1]))
+                if (departure.tomorrow)
+                    departureTime.add(Calendar.DAY_OF_MONTH, 1)
+                val interval = (departureTime.timeInMillis - now.timeInMillis) / (1000 * 60)
+                if (interval in 0..(minInterval - 1)) {
+                    minInterval = (departureTime.timeInMillis - now.timeInMillis) / (1000 * 60)
+                    minDeparture = departure
+                }
+            }
+
+            return minDeparture
+        }
+        private set
+}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d97e13817f530006d3a3d4537557e9bedb0f374a
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
@@ -0,0 +1,75 @@
+package ml.adamsprogs.bimba.models
+
+import android.content.Context
+import android.support.v7.widget.PopupMenu
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import ml.adamsprogs.bimba.R
+import android.view.LayoutInflater
+import java.util.*
+
+
+class FavouritesAdapter(val context: Context, val favourites: List<Favourite>) :
+        RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() {
+    override fun getItemCount(): Int {
+        return favourites.size
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
+
+        val favourite = favourites[position]
+        holder?.nameTextView?.text = favourite.name
+        val nextDeparture = favourite.nextDeparture ?: return
+        val now = Calendar.getInstance()
+        val departureTime = Calendar.getInstance()
+        departureTime.set(Calendar.HOUR_OF_DAY, Integer.parseInt(nextDeparture.time.split(":")[0]))
+        departureTime.set(Calendar.MINUTE, Integer.parseInt(nextDeparture.time.split(":")[1]))
+        if (nextDeparture.tomorrow)
+            departureTime.add(Calendar.DAY_OF_MONTH, 1)
+        val interval = (departureTime.timeInMillis - now.timeInMillis) / (1000 * 60)
+        holder?.timeTextView?.text = context.getString(R.string.departure_in, interval.toString())
+        holder?.lineTextView?.text = context.getString(R.string.departure_to_line, nextDeparture.line, nextDeparture.direction)
+        holder?.moreButton?.setOnClickListener {
+            val popup = PopupMenu(context, it)
+            val inflater = popup.menuInflater
+            popup.setOnMenuItemClickListener {
+                when (it.itemId) {
+                    R.id.favourite_edit -> editFavourite(favourite.name)
+                    R.id.favourite_delete -> deleteFavourite(favourite.name)
+                    else -> false
+                }
+            }
+            inflater.inflate(R.menu.favourite_actions, popup.menu)
+            popup.show()
+        }
+    }
+
+    private fun editFavourite(name: String): Boolean {
+        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+        return true
+    }
+
+    private fun deleteFavourite(name: String): Boolean {
+        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+        return true
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
+        val context = parent?.context
+        val inflater = LayoutInflater.from(context)
+
+        val rowView = inflater.inflate(R.layout.row_favourite, parent, false)
+        val viewHolder = ViewHolder(rowView)
+        return viewHolder
+    }
+
+    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val nameTextView = itemView.findViewById(R.id.favourite_name) as TextView
+        val timeTextView = itemView.findViewById(R.id.favourite_time) as TextView
+        val lineTextView = itemView.findViewById(R.id.favourite_line) as TextView
+        val moreButton = itemView.findViewById(R.id.favourite_more_button) as ImageView
+    }
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
index 96a416a9780a934fb7bd6694c1019ffbf3098cb4..2117cdd3dbd5a679c3f0ffb51d6b55e37a610802 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
@@ -67,13 +67,18 @@         cursor.close()
         return name
     }
 
-    fun getStopDepartures(stopId: String): HashMap<String, ArrayList<Departure>>? {
+    fun getStopDepartures(stopId: String, lineId: String? = null): HashMap<String, ArrayList<Departure>>? {
         if (db == null)
             return null
+        val andLine:String
+        if (lineId == null)
+            andLine = ""
+        else
+            andLine = "and line_id = '$lineId'"
         val cursor = db!!.rawQuery("select lines.number, mode, substr('0'||hour, -2) || ':' || " +
                 "substr('0'||minute, -2) as time, lowFloor, modification, headsign from departures join " +
                 "timetables on(timetable_id = timetables.id) join lines on(line_id = lines.id) where " +
-                "stop_id = ? order by mode, time;", listOf(stopId).toTypedArray())
+                "stop_id = ? $andLine order by mode, time;", listOf(stopId).toTypedArray())
         val departures = HashMap<String, ArrayList<Departure>>()
         departures.put("workdays", ArrayList())
         departures.put("saturdays", ArrayList())
@@ -95,7 +100,7 @@                 "stops on(stop_id = stops.id) where stops.id = ?;",
                 listOf(stopId).toTypedArray())
         val lines = ArrayList<String>()
         while (cursor.moveToNext()) {
-            lines.add(cursor.getString(1))
+            lines.add(cursor.getString(0))
         }
         cursor.close()
         return lines




diff --git a/app/src/main/res/drawable/ic_favourite_empty.xml b/app/src/main/res/drawable/ic_favourite_empty.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b36536b997c2bac42f2b4b09bae0e2de03d08917
--- /dev/null
+++ b/app/src/main/res/drawable/ic_favourite_empty.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
+</vector>




diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e9d756cad67783d7ac68687ef1d196ed887bbeba
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"
+        android:fillColor="#000000"/>
+</vector>




diff --git a/app/src/main/res/layout/activity_dash.xml b/app/src/main/res/layout/activity_dash.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f742d2c57060380e3d98edf399bdde2cd36d4001
--- /dev/null
+++ b/app/src/main/res/layout/activity_dash.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/swipeRefreshLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="ml.adamsprogs.bimba.activities.DashActivity">
+
+    <android.support.constraint.ConstraintLayout
+        android:id="@+id/main_layout"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.arlib.floatingsearchview.FloatingSearchView
+            android:id="@+id/search_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:floatingSearch_close_search_on_keyboard_dismiss="true"
+            app:floatingSearch_leftActionMode="showSearch"
+            app:floatingSearch_searchBarMarginLeft="16dp"
+            app:floatingSearch_searchBarMarginRight="16dp"
+            app:floatingSearch_searchBarMarginTop="16dp"
+            app:floatingSearch_searchHint="@string/search_placeholder"
+            app:floatingSearch_showSearchKey="false"
+            app:floatingSearch_suggestionsListAnimDuration="250" />
+
+
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/favouritesList"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scrollbars="none"
+            android:layout_marginTop="128dp"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            android:layout_marginStart="8dp"
+            app:layout_constraintEnd_toEndOf="parent"
+            android:layout_marginEnd="8dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            android:layout_marginBottom="8dp" />
+
+    </android.support.constraint.ConstraintLayout>
+</android.support.v4.widget.SwipeRefreshLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index c18b6115de3cd974a2f3fac976630ff35e48beab..0000000000000000000000000000000000000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/swipeRefreshLayout"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context="ml.adamsprogs.bimba.activities.MainActivity">
-
-    <android.support.constraint.ConstraintLayout
-        android:id="@+id/main_layout"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-
-        <com.arlib.floatingsearchview.FloatingSearchView
-            android:id="@+id/search_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            app:floatingSearch_close_search_on_keyboard_dismiss="true"
-            app:floatingSearch_leftActionMode="showSearch"
-            app:floatingSearch_searchBarMarginLeft="16dp"
-            app:floatingSearch_searchBarMarginRight="16dp"
-            app:floatingSearch_searchBarMarginTop="16dp"
-            app:floatingSearch_searchHint="@string/search_placeholder"
-            app:floatingSearch_showSearchKey="false"
-            app:floatingSearch_suggestionsListAnimDuration="250" />
-
-
-        <android.support.v7.widget.RecyclerView
-            android:id="@+id/favouritesList"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:scrollbars="none"
-            android:layout_marginTop="128dp"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            android:layout_marginStart="8dp"
-            app:layout_constraintEnd_toEndOf="parent"
-            android:layout_marginEnd="8dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            android:layout_marginBottom="8dp" />
-
-    </android.support.constraint.ConstraintLayout>
-</android.support.v4.widget.SwipeRefreshLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/activity_stop.xml b/app/src/main/res/layout/activity_stop.xml
index b36285d541bc1d4833dbe2550150d8bc7630f925..a56ab1e2d284b956c2fb13106f8d74bd7d2eabcc 100644
--- a/app/src/main/res/layout/activity_stop.xml
+++ b/app/src/main/res/layout/activity_stop.xml
@@ -65,7 +65,6 @@         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="end|bottom"
         android:layout_margin="@dimen/fab_margin"
-        android:visibility="gone"
         app:srcCompat="@drawable/ic_favourite" />
 
 </android.support.design.widget.CoordinatorLayout>




diff --git a/app/src/main/res/layout/departure_row.xml b/app/src/main/res/layout/departure_row.xml
deleted file mode 100644
index d6b0a4371cce9c0c482bfb3c53e52a97724d6028..0000000000000000000000000000000000000000
--- a/app/src/main/res/layout/departure_row.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<android.support.constraint.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    tools:layout_editor_absoluteY="25dp"
-    tools:layout_editor_absoluteX="0dp">
-
-    <TextView
-        android:id="@+id/lineNumber"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text=""
-        android:textAppearance="@style/TextAppearance.AppCompat.Title"
-        app:layout_constraintStart_toStartOf="parent"
-        android:layout_marginStart="8dp"
-        app:layout_constraintTop_toTopOf="parent"
-        android:layout_marginTop="8dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="8dp"
-        app:layout_constraintEnd_toStartOf="@+id/departureTime"
-        android:layout_marginEnd="8dp" />
-
-    <ImageView
-        android:id="@+id/departureTypeIcon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="8dp"
-        android:layout_marginTop="8dp"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        android:contentDescription="@string/departure_type_icon_description" />
-
-    <TextView
-        android:id="@+id/departureTime"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text=""
-        android:textAppearance="@style/TextAppearance.AppCompat.Headline"
-        android:layout_marginTop="8dp"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toEndOf="parent"
-        android:layout_marginStart="64dp" />
-
-    <TextView
-        android:id="@+id/departureDirection"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text=""
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="8dp"
-        app:layout_constraintTop_toBottomOf="@+id/departureTime"
-        app:layout_constraintStart_toEndOf="parent"
-        android:layout_marginStart="64dp" />
-</android.support.constraint.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/row_departure.xml b/app/src/main/res/layout/row_departure.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d6b0a4371cce9c0c482bfb3c53e52a97724d6028
--- /dev/null
+++ b/app/src/main/res/layout/row_departure.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:layout_editor_absoluteY="25dp"
+    tools:layout_editor_absoluteX="0dp">
+
+    <TextView
+        android:id="@+id/lineNumber"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text=""
+        android:textAppearance="@style/TextAppearance.AppCompat.Title"
+        app:layout_constraintStart_toStartOf="parent"
+        android:layout_marginStart="8dp"
+        app:layout_constraintTop_toTopOf="parent"
+        android:layout_marginTop="8dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:layout_marginBottom="8dp"
+        app:layout_constraintEnd_toStartOf="@+id/departureTime"
+        android:layout_marginEnd="8dp" />
+
+    <ImageView
+        android:id="@+id/departureTypeIcon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:layout_marginTop="8dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        android:contentDescription="@string/departure_type_icon_description" />
+
+    <TextView
+        android:id="@+id/departureTime"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text=""
+        android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+        android:layout_marginTop="8dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toEndOf="parent"
+        android:layout_marginStart="64dp" />
+
+    <TextView
+        android:id="@+id/departureDirection"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text=""
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:layout_marginBottom="8dp"
+        app:layout_constraintTop_toBottomOf="@+id/departureTime"
+        app:layout_constraintStart_toEndOf="parent"
+        android:layout_marginStart="64dp" />
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/row_favourite.xml b/app/src/main/res/layout/row_favourite.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e1a3189141bc6f2d966cdb4088f4539ecd20a8c4
--- /dev/null
+++ b/app/src/main/res/layout/row_favourite.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:layout_editor_absoluteX="8dp"
+    tools:layout_editor_absoluteY="128dp">
+
+    <android.support.constraint.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <TextView
+            android:id="@+id/favourite_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="21dp"
+            android:text="aaaa"
+            android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
+            app:layout_constraintStart_toStartOf="@+id/favourite_time"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:layout_editor_absoluteX="16dp"
+            tools:layout_editor_absoluteY="21dp" />
+
+        <TextView
+            android:id="@+id/favourite_line"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="16dp"
+            android:layout_marginStart="16dp"
+            android:text="bbbbb"
+            android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/favourite_time"
+            tools:layout_editor_absoluteX="16dp"
+            tools:layout_editor_absoluteY="78dp" />
+
+        <TextView
+            android:id="@+id/favourite_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="ccccc"
+            android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+            app:layout_constraintStart_toStartOf="@+id/favourite_line"
+            app:layout_constraintTop_toBottomOf="@+id/favourite_name"
+            tools:layout_editor_absoluteX="16dp"
+            tools:layout_editor_absoluteY="46dp" />
+
+        <ImageView
+            android:id="@+id/favourite_more_button"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginEnd="16dp"
+            android:layout_marginTop="16dp"
+            android:contentDescription="@string/favourite_row_more_button"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/ic_more"
+            tools:layout_editor_absoluteX="328dp"
+            tools:layout_editor_absoluteY="16dp" />
+    </android.support.constraint.ConstraintLayout>
+</android.support.v7.widget.CardView>
\ No newline at end of file




diff --git a/app/src/main/res/menu/favourite_actions.xml b/app/src/main/res/menu/favourite_actions.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bf229e060737d7e460e0ce3529001eec33b63c61
--- /dev/null
+++ b/app/src/main/res/menu/favourite_actions.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/favourite_edit"
+        android:orderInCategory="100"
+        android:title="@string/action_edit" />
+    <item
+        android:id="@+id/favourite_delete"
+        android:orderInCategory="200"
+        android:title="@string/action_delete" />
+</menu>
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 01718d6790b7e077858dbdceef2f2025c4097d4e..59eab5f8398508474e4978a95a5c2ea60d22b81b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -10,6 +10,7 @@     Type
     <string name="departure_type_icon_description" translatable="false">departure type (timetable, VM)</string>
     <string name="departure_in">In %1$s minutes</string>
     <string name="departure_to">→ %1$s</string>
+    <string name="departure_to_line">%1$s → %2$s</string>
     <string name="departure_at">At %1$s</string>
     <string name="no_database_background" translatable="false">no database background</string>
     <string name="no_db_connect">Connect to the Internet to download the timetable</string>
@@ -22,4 +23,7 @@     Downloaded timetable is corrupted – can’t update
     <string name="error_try_later">Error. Try again later</string>
     <string name="now">Now</string>
     <string name="stop_already_fav">This stop is already in favourites</string>
+    <string name="favourite_row_more_button" translatable="false">favourite row more button</string>
+    <string name="action_edit">Edit</string>
+    <string name="action_delete">Delete</string>
 </resources>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 3ab41cc7ffb8d0e04daa8ddc5362e1b1ae7bb473..d95d96583cbfd31bc307f6f239448f4967e10343 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -19,4 +19,7 @@     Brak połączenia z Internetem – nie można zaktualizować rozkładu
     <string name="error_try_later">Błąd. Spróbuj ponownie później</string>
     <string name="now">Teraz</string>
     <string name="stop_already_fav">Ten przystanek już jest pośród ulubionych</string>
+    <string name="departure_to_line">%1$s → %2$s</string>
+    <string name="action_delete">Usuń</string>
+    <string name="action_edit">Edytuj</string>
 </resources>
\ No newline at end of file




diff --git a/research/scraper.py b/research/scraper.py
index 96b855fb4275f76a18daad2486598937c3391e31..37dd35d799fddc496ec0b2f9e1816f63785707d9 100755
--- a/research/scraper.py
+++ b/research/scraper.py
@@ -16,24 +16,47 @@ import sys
 import time
 import requests
 from bs4 import BeautifulSoup
+import secrets
 
-def get_nodes():
+
+def remove_options(text):
+    return re.sub('(<select[^>]*>([^<]*<option[^>]*>[^<]*</option>)+[^<]*</select>)','', text)
+
+
+def get_validity():
+    """
+    get timetable validity
+    """
+    session = requests.session()
+    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/index', verify='bundle.pem')
+    option = re.search('<option value="[0-9]{8}" selected', index.text).group()
+    return option.split('"')[1]
+
+
+def get_nodes(checksum):
     """
     get nodes
     """
     session = requests.session()
 
     index = session.get('https://www.ztm.poznan.pl/goeuropa-api/all-nodes', verify='bundle.pem')
-    return [(stop['symbol'], stop['name']) for stop in json.loads(index.text)]
+    new_checksum = hashlib.sha512(index.text.encode('utf-8')).hexdigest()
+    if checksum == new_checksum:
+        return None
+    return [(stop['symbol'], stop['name']) for stop in json.loads(index.text)], new_checksum
 
 
-def get_stops(node):
+def get_stops(node, checksum):
     """
     get stops
     """
     session = requests.session()
 
-    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/node_stops/{}'.format(node), verify='bundle.pem')
+    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/node_stops/{}'.format(node),
+                        verify='bundle.pem')
+    new_checksum = hashlib.sha512(index.text.encode('utf-8')).hexdigest()
+    if checksum == new_checksum:
+        return None
     stops = []
     for stop in json.loads(index.text):
         stop_id = stop['stop']['id']
@@ -43,22 +66,27 @@         lon = stop['stop']['lon']
         directions = ', '.join(['{} → {}'.format(transfer['name'], transfer['headsign'])
                                 for transfer in stop['transfers']])
         stops.append((stop_id, node, number, lat, lon, directions))
-    return stops
+    return stops, new_checksum
 
 
-def get_lines():
+def get_lines(checksum):
     """
     get lines
     """
     session = requests.session()
 
     index = session.get('https://www.ztm.poznan.pl/goeuropa-api/index', verify='bundle.pem')
-    soup = BeautifulSoup(index.text, 'html.parser')
+    index = re.sub('route-modal-[0-9a-f]{7}', '', index.text)
+    index = remove_options(index)
+    new_checksum = hashlib.sha512(index.encode('utf-8')).hexdigest()
+    if new_checksum == checksum:
+        return None
+    soup = BeautifulSoup(index, 'html.parser')
 
     lines = {line['data-lineid']: line.text for line in
              soup.findAll(attrs={'class': re.compile(r'.*\blineNo-bt\b.*')})}
 
-    return lines
+    return lines, new_checksum
 
 
 def get_route(line_id):
@@ -67,7 +95,8 @@     get routes
     """
     session = requests.session()
 
-    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/line-info/{}'.format(line_id), verify='bundle.pem')
+    index = session.get('https://www.ztm.poznan.pl/goeuropa-api/line-info/{}'.format(line_id),
+                        verify='bundle.pem')
     soup = BeautifulSoup(index.text, 'html.parser')
     directions = soup.findAll(attrs={'class': re.compile(r'.*\baccordion-item\b.*')})
     routes = {}
@@ -80,15 +109,21 @@         routes[direction_id] = route
     return routes
 
 
-def get_stop_times(stop_id, line_id, direction_id):
+def get_stop_times(stop_id, line_id, direction_id, checksum):
     """
     get timetable
     """
     session = requests.session()
 
     index = session.post('https://www.ztm.poznan.pl/goeuropa-api/stop-info/{}/{}'.
-                         format(stop_id, line_id), data={'directionId': direction_id}, verify='bundle.pem')
-    soup = BeautifulSoup(index.text, 'html.parser')
+                         format(stop_id, line_id), data={'directionId': direction_id},
+                         verify='bundle.pem')
+    index = re.sub('route-modal-[0-9a-f]{7}', '', index.text)
+    index = remove_options(index)
+    new_checksum = hashlib.sha512(index.encode('utf-8')).hexdigest()
+    if new_checksum == checksum:
+        return None
+    soup = BeautifulSoup(index, 'html.parser')
     legends = {}
     for row in soup.find(attrs={'class': re.compile(r'.*\blegend-box\b.*')}).findAll('li'):
         row = row.text.split('-')
@@ -111,7 +146,7 @@             for dep in deps:
                 schedule.append((hour, *describe(dep['time'], legends), dep['lowFloor']))
         schedules[mode_name] = schedule
 
-    return schedules, hashlib.sha512(index.text.encode('utf-8')).hexdigest()
+    return schedules, new_checksum
 
 
 def describe(dep_time, legend):
@@ -130,68 +165,146 @@ def main():
     """
     main function
     """
+    updating = False
+    changed = False
     if os.path.exists('timetable.db'):
-        return
+        updating = True
+
 
     print(time.time())
     with sqlite3.connect('timetable.db') as connection:
-        print('creating tables')
-        cursor = connection.cursor()
-        cursor.execute('create table nodes(symbol TEXT PRIMARY KEY, name TEXT)')
-        cursor.execute('create table stops(id TEXT PRIMARY KEY, symbol TEXT \
-                        references node(symbol), number TEXT, lat REAL, lon REAL, headsigns TEXT)')
-        cursor.execute('create table lines(id TEXT PRIMARY KEY, number TEXT)')
-        cursor.execute('create table timetables(id INTEGER PRIMARY KEY, stop_id \
-                        TEXT references stop(id), line_id TEXT references line(id), \
-                        headsign TEXT, checksum TEXT)')
-        cursor.execute('create table departures(id INTEGER PRIMARY KEY, timetable_id INTEGER \
-                        references timetable(id), hour INTEGER, minute INTEGER, mode TEXT, \
-                        lowFloor INTEGER, modification TEXT)')
+        try:
+            cursor = connection.cursor()
+            if updating:
+                cursor.execute("select value from metadata where key = 'validFrom'")
+                current_valid_from = cursor.fetchone()[0]
+                if get_validity() <= current_valid_from:
+                    return 304
+            else:
+                print('creating tables')
+                cursor.execute('create table metadata(key TEXT PRIMARY KEY, value TEXT)')
+                cursor.execute('create table checksums(checksum TEXT, for TEXT, id TEXT)')
+                cursor.execute('create table nodes(symbol TEXT PRIMARY KEY, name TEXT)')
+                cursor.execute('create table stops(id TEXT PRIMARY KEY, symbol TEXT \
+                                references node(symbol), number TEXT, lat REAL, lon REAL, \
+                                headsigns TEXT)')
+                cursor.execute('create table lines(id TEXT PRIMARY KEY, number TEXT)')
+                cursor.execute('create table timetables(id TEXT PRIMARY KEY, stop_id TEXT references stop(id), \
+                                line_id TEXT references line(id), headsign TEXT)')
+                cursor.execute('create table departures(id INTEGER PRIMARY KEY, \
+                                timetable_id TEXT references timetable(id), \
+                                hour INTEGER, minute INTEGER, mode TEXT, \
+                                lowFloor INTEGER, modification TEXT)')
+
+            print('getting validity')
+            cursor.execute("delete from metadata where key = 'validFrom'")
+            cursor.execute("insert into metadata values('validFrom', ?)", (get_validity(),))
+            print('getting nodes')
+            cursor.execute("select checksum from checksums where for = 'nodes'")
+            checksum = cursor.fetchone()
+            if checksum != None:
+                checksum = checksum[0]
+            else:
+                checksum = ''
+            nodes_result = get_nodes(checksum)
+            if nodes_result is not None:
+                nodes, checksum = nodes_result
+                cursor.execute('delete from nodes')
+                cursor.execute("delete from checksums where for = 'nodes'")
+                cursor.execute("insert into checksums values(?, 'nodes', null)", (checksum,))  # update
+                cursor.executemany('insert into nodes values(?, ?)', nodes)
+                changed = True
+            else:
+                cursor.execute('select * from nodes')
+                nodes = cursor.fetchall()
+                nodes = [(sym, nam) for sym, nam, _ in nodes]
+            nodes_no = len(nodes)
+            print('getting stops')
+            node_i = 1
+            for symbol, _ in nodes:
+                print('\rnode {}/{}'.format(node_i, nodes_no), end='')
+                sys.stdout.flush()
+                cursor.execute("select checksum from checksums where for = 'node' and id = ?", (symbol,))
+                checksum = cursor.fetchone()
+                if checksum != None:
+                    checksum = checksum[0]
+                else:
+                    checksum = ''
+                stops_result = get_stops(symbol, checksum)
+                if stops_result is not None:
+                    stops, checksum = stops_result
+                    cursor.execute('delete from stops where symbol = ?', (symbol,))
+                    cursor.executemany('insert into stops values(?, ?, ?, ?, ?, ?)', stops)
+                    cursor.execute("update checksums set checksum = ? where for = 'node' and id = ?", (checksum, symbol))
+                    changed = True
+                node_i += 1
+            print('\ngetting lines')
+            cursor.execute("select checksum from checksums where for = 'lines'")
+            checksum = cursor.fetchone()
+            if checksum != None:
+                checksum = checksum[0]
+            else:
+                checksum = ''
+            lines_result = get_lines(checksum)
+            if lines_result is not None:
+                lines, checksum = lines_result
+                cursor.execute('delete from lines')
+                cursor.execute("delete from checksums where for = 'lines'")
+                cursor.execute("insert into checksums values(?, 'lines', null)", (checksum,))  # update
+                cursor.executemany('insert into lines values(?, ?)', lines.items())
+                changed = True
+            else:
+                cursor.execute('select * from lines')
+                lines = cursor.fetchall()
 
-        print('getting nodes')
-        nodes = get_nodes()
-        cursor.executemany('insert into nodes values(?, ?);', nodes)
-        nodes_no = len(nodes)
-        print('getting stops')
-        node_i = 1
-        for symbol, _ in nodes:
-            print('\rstop {}/{}'.format(node_i, nodes_no), end='')
-            sys.stdout.flush()
-            cursor.executemany('insert into stops values(?, ?, ?, ?, ?, ?);', get_stops(symbol))
-            node_i += 1
-        print('')
-        lines = get_lines()
-        lines_no = len(lines)
-        line_i = 1
-        tti = 0
-        cursor.executemany('insert into lines values(?, ?);', lines.items())
-        for line_id, _ in lines.items():
-            route = get_route(line_id)
-            routes_no = len(route)
-            route_i = 1
-            for direction, stops in route.items():
-                stops_no = len(stops)
-                stop_i = 1
-                for stop in stops:
-                    print('line {}/{} route {}/{} stop {}/{}'.
-                          format(line_i, lines_no, route_i, routes_no, stop_i, stops_no), end='')
-                    sys.stdout.flush()
-                    timetables, checksum = get_stop_times(stop['id'], line_id, direction)
-                    cursor.execute('insert into timetables values(?, ?, ?, ?, ?);',
-                                   (tti, stop['id'], line_id, stops[-1]['name'], checksum))
-                    for mode, times in timetables.items():
-                        cursor.executemany('insert into departures values(null, ?, ?, ?, ?, ?, ?);',
-                                           [(tti, hour, minute, mode, lowfloor, desc)
-                                            for hour, minute, desc, lowfloor in times])
-                    stop_i += 1
-                    tti += 1
-                    print('{}\r'.format(' '*35), end='')
-                    sys.stdout.flush()
-                route_i += 1
-            print('')
-            line_i += 1
+            lines_no = len(lines)
+            line_i = 1
+            for line_id, _ in lines.items():
+                route = get_route(line_id)
+                routes_no = len(route)
+                route_i = 1
+                for direction, stops in route.items():
+                    stops_no = len(stops)
+                    stop_i = 1
+                    for stop in stops:
+                        timetable_id = secrets.token_hex(4)
+                        print('line {}/{} route {}/{} stop {}/{}'.
+                              format(line_i, lines_no, route_i, routes_no, stop_i, stops_no), end='')
+                        sys.stdout.flush()
+                        cursor.execute("select checksum from checksums where for = 'timetable' and id = ?", (timetable_id,))
+                        checksum = cursor.fetchone()
+                        if checksum != None:
+                            checksum = checksum[0]
+                        else:
+                            checksum = ''
+                        stop_times = get_stop_times(stop['id'], line_id, direction, checksum)
+                        if stop_times is not None:
+                            timetables, checksum = stop_times
+                            cursor.execute('delete from timetables where line_id = ? and stop_id = ?',
+                                           (line_id, stop['id']))
+                            cursor.execute('insert into timetables values(?, ?, ?, ?)',
+                                           (timetable_id, stop['id'], line_id, stops[-1]['name']))
+                            cursor.execute("insert into checksums values(?, 'timetable', ?)", (checksum, timetable_id))
+                            changed = True
+                            cursor.execute('delete from departures where timetable_id = ?',
+                                           (timetable_id,))
+                            for mode, times in timetables.items():
+                                cursor.executemany('insert into departures values(null, ?, ?, ?, ?, ?, \
+                                                    ?)', [(timetable_id, hour, minute, mode, lowfloor, desc)
+                                                          for hour, minute, desc, lowfloor in times])
+                        stop_i += 1
+                        print('{}\r'.format(' '*35), end='')
+                        sys.stdout.flush()
+                    route_i += 1
+                print('')
+                line_i += 1
+        except KeyboardInterrupt:
+            return 404
     print(time.time())
+    if changed:
+        return 0
+    return 304
 
 
 if __name__ == '__main__':
-    main()
+    exit(main())