Bimba.git

commit 263c225075fa1ed9fe861d4e694ecefca090209a

Author: Adam <git@apiote.xyz>

choose Bimba server and feeds

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8d4aaff42578cccac40b793bd26d41d6b93a4b93..d49ebffd9f8b3c10f8e253c32395f43bf2f20289 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,6 +18,18 @@ 		android:supportsRtl="true"
 		android:theme="@style/Theme.Bimba.Style"
 		tools:targetApi="31">
 		<activity
+			android:name=".feeds.FeedChooserActivity"
+			android:exported="false" />
+		<activity
+			android:name=".FirstRunActivity"
+			android:exported="true">
+			<intent-filter>
+				<action android:name="android.intent.action.MAIN" />
+
+				<category android:name="android.intent.category.LAUNCHER" />
+			</intent-filter>
+		</activity>
+		<activity
 			android:name=".departures.DeparturesActivity"
 			android:exported="false" />
 		<activity
@@ -27,13 +39,8 @@ 			android:label="@string/title_activity_results"
 			android:theme="@style/Theme.Bimba.Style" />
 		<activity
 			android:name=".dashboard.MainActivity"
-			android:exported="true"
+			android:exported="false"
 			android:windowSoftInputMode="adjustPan">
-			<intent-filter>
-				<action android:name="android.intent.action.MAIN" />
-
-				<category android:name="android.intent.category.LAUNCHER" />
-			</intent-filter>
 		</activity>
 	</application>
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/FirstRunActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/FirstRunActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7cd423c91d4152df677f57e69b27d07bb473d234
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/FirstRunActivity.kt
@@ -0,0 +1,24 @@
+package ml.adamsprogs.bimba
+
+import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import ml.adamsprogs.bimba.dashboard.MainActivity
+import ml.adamsprogs.bimba.feeds.FeedChooserActivity
+
+class FirstRunActivity : AppCompatActivity() {
+	override fun onCreate(savedInstanceState: Bundle?) {
+		installSplashScreen()
+		super.onCreate(savedInstanceState)
+
+		val preferences = getSharedPreferences("shp", MODE_PRIVATE)
+		val intent = if (preferences.getBoolean("firstRun", true)) {
+			Intent(this, FeedChooserActivity::class.java)
+		} else {
+			Intent(this, MainActivity::class.java)
+		}
+		startActivity(intent)
+		finish()
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt
index 76f2a4096a43a1b61b786cf62b0330a38245b88c..8141e3022c5c7737454c10e19ec6c0765c430d8a 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt
@@ -8,28 +8,53 @@ import java.net.HttpURLConnection
 import java.net.URL
 import java.net.URLEncoder
 
-suspend fun queryItems(query: String): InputStream? {
-	return request("https://bimba.apiote.xyz", "poznan_ztm", "items", mapOf("q" to query))
+data class Server(val host: String, val token: String, val feeds: String)
+
+@Suppress("BlockingMethodInNonBlockingContext")
+suspend fun getFeeds(server: Server): InputStream? {  // todo if 401 then needs token
+	return rawRequest(URL("${hostWithScheme(server.host)}/api/"), server)
 }
 
-suspend fun locateItems(plusCode: String): InputStream? {
-	return request("https://bimba.apiote.xyz", "poznan_ztm", "items", mapOf("near" to plusCode))
+suspend fun queryItems(server: Server, query: String): InputStream? {
+	return request(server, "items", mapOf("q" to query))
 }
 
-suspend fun getDepartures(stop: String, line: String?): InputStream? {
-	return request("https://bimba.apiote.xyz", "poznan_ztm", "departures", mapOf("code" to stop))
+suspend fun locateItems(server: Server, plusCode: String): InputStream? {
+	return request(server, "items", mapOf("near" to plusCode))
+}
+
+suspend fun getDepartures(server: Server, stop: String, line: String?): InputStream? {
+	val params = mutableMapOf("code" to stop)
+	if (line != null) {
+		params["line"] = line
+	}
+	return request(server, "departures", params)
+}
+
+@Suppress("BlockingMethodInNonBlockingContext")
+suspend fun rawRequest(url: URL, server: Server): InputStream? {
+	return withContext(Dispatchers.IO) {
+		val c = (url.openConnection() as HttpURLConnection).apply {
+			setRequestProperty("X-Bimba-Token", server.token)
+		}
+		try {
+			c.inputStream
+		} catch (e: Exception) {
+			Log.e("request", e.stackTraceToString())
+			null
+		}
+	}
 }
 
 @Suppress("BlockingMethodInNonBlockingContext")
 suspend fun request(
-	host: String,
-	feed: String,
+	server: Server,
 	resource: String,
 	params: Map<String, String>
 ): InputStream? {
 	return withContext(Dispatchers.IO) {
 		val url = URL(
-			"$host/api/$feed/$resource${
+			"${hostWithScheme(server.host)}/api/${server.feeds}/$resource${
 				params.map {
 					"${it.key}=${
 						URLEncoder.encode(
@@ -40,14 +65,13 @@ 					}"
 				}.joinToString("&", "?")
 			}"
 		)
-		val c = (url.openConnection() as HttpURLConnection).apply {
-			setRequestProperty("X-Bimba-Token", "ef0179272e7270e1a2da1710a8ba24e1")
-		}
-		try {
-			c.inputStream
-		} catch (e: Exception) {
-			Log.e("request", e.stackTraceToString())
-			null
-		}
+		rawRequest(url, server)
 	}
-}
\ No newline at end of file
+}
+
+fun hostWithScheme(host: String): String =
+	if (host.startsWith("http://") or host.startsWith("https://")) {
+		host
+	} else {
+		"https://$host"
+	}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt
index 30173fc3e7147a28c49168583853b62f3d1aab17..c909c287b157221e2c2104426ca740a7aca8657f 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt
@@ -101,6 +101,47 @@ 		}
 	}
 }
 
+interface FeedsResponse {
+	companion object {
+		fun unmarshal(stream: InputStream): FeedsResponse {
+			val reader = Reader(stream)
+			when (reader.readUInt()) {
+				0UL -> {
+					TODO("error response")
+				}
+				1UL -> {
+					return FeedsSuccess.unmarshal(stream)
+				}
+				else -> {
+					TODO("throw unknown tag")
+				}
+			}
+		}
+	}
+}
+
+data class FeedsSuccess(
+	val adminContact: String,
+	val rateLimited: Boolean,
+	val private: Boolean,
+	val feeds: List<FeedInfo>
+) : FeedsResponse {
+	companion object {
+		fun unmarshal(stream: InputStream): FeedsSuccess {
+			val feeds = mutableListOf<FeedInfo>()
+			val reader = Reader(stream)
+			val adminContact = reader.readString()
+			val rateLimited = reader.readBoolean()
+			val private = reader.readBoolean()
+			val itemsNum = reader.readUInt()
+			for (i in 0UL until itemsNum) {
+				feeds.add(FeedInfo.unmarshal(stream))
+			}
+			return FeedsSuccess(adminContact, rateLimited, private, feeds)
+		}
+	}
+}
+
 data class Error(val message: String) : ItemsResponse, DeparturesResponse {
 
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
index dc1bae961eb42ae229d5e6d66a7935b31f1db9bd..f3f8729221329c304cfa34577e5f0c402b4351de 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
@@ -12,6 +12,27 @@ import kotlin.math.abs
 import kotlin.math.pow
 import kotlin.math.roundToInt
 
+data class FeedInfo(
+	val name: String,
+	val id: String,
+	val attribution: String,
+	val description: String,
+	val lastUpdate: String // todo date from RFC
+) {
+	companion object {
+		fun unmarshal(stream: InputStream): FeedInfo {
+			val reader = Reader(stream)
+			return FeedInfo(
+				reader.readString(),
+				reader.readString(),
+				reader.readString(),
+				reader.readString(),
+				reader.readString()
+			)
+		}
+	}
+}
+
 data class Alert(
 	val header: String,
 	val Description: String,




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeFragment.kt
index bb2fdec0e9a13aa35dc468fc7bbfd1c8be215d82..21d9a6aed2b9609ca89d5ba99509f1f4eee9a4ba 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeFragment.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeFragment.kt
@@ -1,5 +1,6 @@
 package ml.adamsprogs.bimba.dashboard.ui.home
 
+import android.content.SharedPreferences
 import android.os.Bundle
 import android.os.Handler
 import android.os.Looper
@@ -8,6 +9,7 @@ import android.text.TextWatcher
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModelProvider
 import com.mancj.materialsearchbar.MaterialSearchBar
@@ -15,6 +17,7 @@ import com.mancj.materialsearchbar.MaterialSearchBar.BUTTON_NAVIGATION
 import ml.adamsprogs.bimba.search.BimbaSuggestionsAdapter
 import ml.adamsprogs.bimba.dashboard.MainActivity
 import ml.adamsprogs.bimba.api.Item
+import ml.adamsprogs.bimba.api.Server
 import ml.adamsprogs.bimba.databinding.FragmentHomeBinding
 
 class HomeFragment : Fragment() {
@@ -34,11 +37,13 @@
 		_binding = FragmentHomeBinding.inflate(inflater, container, false)
 		val root: View = binding.root
 
+		val shp = requireContext().getSharedPreferences("shp", AppCompatActivity.MODE_PRIVATE)
+
 		binding.searchBar.lastSuggestions = listOf<Item>()
 		homeViewModel.items.observe(viewLifecycleOwner) {
 			binding.searchBar.updateLastSuggestions(it.take(6))  // xxx workaround for suggestions behind navbar; should be paginated server-side
 		}
-		binding.searchBar.addTextChangeListener(SearchBarWatcher(homeViewModel))
+		binding.searchBar.addTextChangeListener(SearchBarWatcher(homeViewModel, shp))  // todo pass shp and host
 		binding.searchBar.setOnSearchActionListener(object : MaterialSearchBar.OnSearchActionListener {
 			override fun onButtonClicked(buttonCode: Int) {
 				when (buttonCode) {
@@ -77,7 +82,7 @@ 		_binding = null
 	}
 }
 
-class SearchBarWatcher(private val homeViewModel: HomeViewModel) :
+class SearchBarWatcher(private val homeViewModel: HomeViewModel, private val shp: SharedPreferences) :
 	TextWatcher {
 	private val handler = Handler(Looper.getMainLooper())
 	private var workRunnable = Runnable {}
@@ -91,8 +96,13 @@
 	override fun afterTextChanged(s: Editable?) {
 		handler.removeCallbacks(workRunnable)
 		workRunnable = Runnable {
+			val host = shp.getString("host", "bimba.apiote.xyz")!!
 			val text = s.toString()
-			homeViewModel.getItems(text)
+			homeViewModel.getItems(
+				Server(
+				host, shp.getString("token", "")!!,
+				shp.getString("${host}_feeds", "")!!
+			), text)
 		}
 		handler.postDelayed(workRunnable, 1000) // todo make good time (probably between 500, 1000ms)
 	}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeViewModel.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeViewModel.kt
index 38855e9f151ec3819b86e03416efd65de74c9048..c6d5594594dd7abd59a9cb183108acb16517c41d 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeViewModel.kt
@@ -15,9 +15,9 @@
 	private val mutableItems = MutableLiveData<List<Item>>()
 	val items: LiveData<List<Item>> = mutableItems
 
-	fun getItems(query: String) {
+	fun getItems(server: Server, query: String) {
 		viewModelScope.launch {
-			val itemsStream = queryItems(query)
+			val itemsStream = queryItems(server, query)
 			if (itemsStream == null) {
 				// todo Toast.makeText(context, "Couldn't get response", Toast.LENGTH_SHORT).show()
 			} else {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
index 001f1d8342f06e3828826971892a4b4b6db61ede..ca814dc1cebfd445554539279afac988f8e8458d 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
@@ -39,14 +39,26 @@ 		}
 		binding.departuresRecycler.adapter = adapter
 		WindowCompat.setDecorFitsSystemWindows(window, false)
 
+		val shp = getSharedPreferences("shp", MODE_PRIVATE)
+		val host = shp.getString("host", "bimba.apiote.xyz")!!
+
 		// todo check every 30s
 
 		MainScope().launch {
 			intent?.extras?.getString("code")?.let {
-				val departuresStream = getDepartures(it, null)
+				val departuresStream = getDepartures(
+					Server(
+						host, shp.getString("token", "")!!,
+						shp.getString("${host}_feeds", "")!!
+					), it, null
+				)
 				if (departuresStream == null) {
 					// todo show empty state
-					Toast.makeText(this@DeparturesActivity as Context, "Couldn't get response", Toast.LENGTH_SHORT).show()
+					Toast.makeText(
+						this@DeparturesActivity as Context,
+						"Couldn't get response",
+						Toast.LENGTH_SHORT
+					).show()
 				} else {
 					updateItems(unmarshallDepartureResponse(departuresStream))
 				}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..be80cba347e523e88e871490b9b4f1ca371bc0e1
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt
@@ -0,0 +1,152 @@
+package ml.adamsprogs.bimba.feeds
+// git:fixup feeds
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.core.content.edit
+import androidx.core.widget.addTextChangedListener
+import androidx.recyclerview.widget.LinearLayoutManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import ml.adamsprogs.bimba.R
+import ml.adamsprogs.bimba.api.FeedsResponse
+import ml.adamsprogs.bimba.api.FeedsSuccess
+import ml.adamsprogs.bimba.api.Server
+import ml.adamsprogs.bimba.api.getFeeds
+import ml.adamsprogs.bimba.dashboard.MainActivity
+import ml.adamsprogs.bimba.databinding.ActivityFeedChooserBinding
+import java.io.InputStream
+
+
+class FeedChooserActivity : AppCompatActivity() {
+	private var _binding: ActivityFeedChooserBinding? = null
+	private val binding get() = _binding!!
+
+	private lateinit var adapter: BimbaFeedInfoAdapter
+	private lateinit var preferences: SharedPreferences
+
+	override fun onCreate(savedInstanceState: Bundle?) {
+		super.onCreate(savedInstanceState)
+		_binding = ActivityFeedChooserBinding.inflate(layoutInflater)
+		setContentView(binding.root)
+
+		preferences = getSharedPreferences("shp", MODE_PRIVATE)
+
+		setUpRecycler()
+
+		binding.button.setOnClickListener {
+			getServer(
+				binding.serverField.editText!!.text.toString(),
+				binding.tokenField.editText!!.text.toString()
+			)
+		}
+
+		binding.serverField.editText!!.apply {
+			setText(preferences.getString("host", "bimba.apiote.xyz"))
+			addTextChangedListener { editable ->
+				binding.button.apply {
+					text = context.getString(R.string.cont)
+					isEnabled = !editable.isNullOrBlank()
+					setOnClickListener {
+						getServer(
+							editable!!.toString(),
+							binding.tokenField.editText!!.text.toString()
+						)
+					}
+				}
+			}
+		}
+	}
+
+	private fun setUpRecycler() {
+		binding.resultsRecycler.layoutManager = LinearLayoutManager(this)
+		adapter = BimbaFeedInfoAdapter(layoutInflater, listOf(), this) {
+			Log.v("FeedInfo", "clicked: $it")
+			// todo show bottom sheet
+		}
+		binding.resultsRecycler.adapter = adapter
+	}
+
+	private fun getServer(host: String, token: String) {
+		assert(binding.serverField.editText!!.text.isNotEmpty())
+
+		preferences.edit(true) {
+			putString("server", host)
+			putString("token", token)
+		}
+
+		binding.circularProgressIndicator.visibility = View.VISIBLE
+		binding.resultsRecycler.visibility = View.GONE
+		binding.feedInfo.visibility = View.GONE
+
+		MainScope().launch {
+			val feedsStream = getFeeds(Server(host, token, ""))
+			if (feedsStream == null) {
+				// todo(error-handling) show empty state
+				Toast.makeText(
+					this@FeedChooserActivity as Context,
+					"Couldn't get response",
+					Toast.LENGTH_SHORT
+				).show()
+			} else {
+				updateItems(unmarshallFeedsResponse(feedsStream))
+				binding.button.apply {
+					text = context.getString(R.string.save)
+					setOnClickListener {
+						moveOn()
+					}
+				}
+			}
+		}
+	}
+
+	private fun moveOn() {
+		val intent = Intent(this, MainActivity::class.java)
+		startActivity(intent)
+		val wasFirstRun = preferences.getBoolean("firstRun", true)
+		preferences.edit(true) {
+			putBoolean("firstRun", false)
+		}
+		if (wasFirstRun) {
+			finish()
+		}
+	}
+
+	private suspend fun unmarshallFeedsResponse(stream: InputStream): FeedsSuccess {
+		return withContext(Dispatchers.IO) {
+			when (val response = FeedsResponse.unmarshal(stream)) {
+				is FeedsSuccess -> {
+					return@withContext response
+				}
+				else -> {
+					TODO("Error response")
+				}
+			}
+		}
+	}
+
+	private fun updateItems(response: FeedsSuccess) {
+		Log.v("items", "${response.adminContact} ${response.rateLimited}")
+		response.feeds.forEach {
+			Log.v("items", "$it")
+		}
+		binding.circularProgressIndicator.visibility = View.GONE
+		binding.resultsRecycler.visibility = View.VISIBLE
+		binding.feedInfo.visibility = View.VISIBLE
+		binding.feedInfo.text =
+			if(response.rateLimited) {
+				getString(R.string.server_info_rate_limited, response.adminContact)
+			} else {
+				getString(R.string.server_info_not_rate_limited, response.adminContact)
+			}
+
+		adapter.update(response.feeds)
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedInfos.kt b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedInfos.kt
new file mode 100644
index 0000000000000000000000000000000000000000..09707bfb08879bbb89bbc881b3e715bd43f2b275
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedInfos.kt
@@ -0,0 +1,77 @@
+package ml.adamsprogs.bimba.feeds
+// git:fixup feeds
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.core.content.edit
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.materialswitch.MaterialSwitch
+import ml.adamsprogs.bimba.R
+import ml.adamsprogs.bimba.api.FeedInfo
+
+
+class BimbaFeedInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+	val root: View = itemView.findViewById(R.id.feedinfo)
+	val switch: MaterialSwitch = itemView.findViewById(R.id.feed_switch)
+	val name: TextView = itemView.findViewById(R.id.feed_name)
+
+	companion object {
+		fun bind(
+			feed: FeedInfo,
+			context: Context,
+			holder: BimbaFeedInfoViewHolder?,
+			onClickListener: (FeedInfo) -> Unit
+		) {
+			val shp = context.getSharedPreferences("shp", MODE_PRIVATE)
+			val host = shp.getString("host", "bimba.apiote.xyz")!!
+			val enabledFeeds =
+				shp.getString("${host}_feeds", "")!!.split(",").associateWith { }.toMutableMap()
+
+			holder?.root?.setOnClickListener {
+				onClickListener(feed)
+			}
+			holder?.name?.text = feed.name
+			holder?.switch?.apply {
+				isChecked = feed.id in enabledFeeds
+				setOnCheckedChangeListener { _, isChecked ->
+					if (isChecked) {
+						enabledFeeds[feed.id] = Unit
+					} else {
+						enabledFeeds.remove(feed.id)
+					}
+					shp.edit(true) {
+						putString("${host}_feeds", enabledFeeds.map { it.key }.filter { it != "" }.joinToString(separator = ","))
+					}
+				}
+			}
+		}
+	}
+}
+
+class BimbaFeedInfoAdapter(
+	private val inflater: LayoutInflater,
+	private var feeds: List<FeedInfo>,
+	private val context: Context,
+	private val onClickListener: ((FeedInfo) -> Unit)
+) :
+	RecyclerView.Adapter<BimbaFeedInfoViewHolder>() {
+	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaFeedInfoViewHolder {
+		val rowView = inflater.inflate(R.layout.feedinfo, parent, false)
+		return BimbaFeedInfoViewHolder(rowView)
+	}
+
+	override fun onBindViewHolder(holder: BimbaFeedInfoViewHolder, position: Int) {
+		BimbaFeedInfoViewHolder.bind(feeds[position], context, holder, onClickListener)
+	}
+
+	override fun getItemCount(): Int = feeds.size
+
+	fun update(items: List<FeedInfo>) {
+		feeds = items
+		notifyDataSetChanged()
+	}
+
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt
index 09164011bdd3a00a8bb7a32d8a8428619fd93deb..eeba8562591e33fb4434dde98e994810f5d18176 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt
@@ -3,6 +3,7 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
+import android.content.SharedPreferences
 import android.location.Location
 import android.location.LocationListener
 import android.location.LocationManager
@@ -31,10 +32,15 @@ 	private val binding get() = _binding!!
 
 	private lateinit var adapter: BimbaResultsAdapter
 
+	private lateinit var shp: SharedPreferences
+
 	override fun onCreate(savedInstanceState: Bundle?) {
 		super.onCreate(savedInstanceState)
 		_binding = ActivityResultsBinding.inflate(layoutInflater)
 		setContentView(binding.root)
+
+		shp = getSharedPreferences("shp", MODE_PRIVATE)
+
 		binding.resultsRecycler.layoutManager = LinearLayoutManager(this)
 		adapter = BimbaResultsAdapter(layoutInflater, this, listOf()) {
 			when (it) {
@@ -64,7 +70,13 @@ 			}
 			Mode.MODE_SEARCH -> {
 				val query = intent.extras?.getString("query")!!
 				supportActionBar?.title = "Results for ‘$query’"
-				getItemsByQuery(query)
+				val host = shp.getString("host", "bimba.apiote.xyz")!!
+				getItemsByQuery(
+					Server(
+						host, shp.getString("token", "")!!,
+						shp.getString("${host}_feeds", "")!!
+					), query
+				)
 			}
 		}
 	}
@@ -83,7 +95,13 @@ 		Log.v("Location", "found $location")
 		val code = OpenLocationCode.encode(location.latitude, location.longitude)
 		val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
 		locationManager.removeUpdates(this)
-		getItemsByLocation(code)
+		val host = shp.getString("host", "bimba.apiote.xyz")!!
+		getItemsByLocation(
+			Server(
+				host, shp.getString("token", "")!!,
+				shp.getString("${host}_feeds", "")!!
+			), code
+		)
 	}
 
 	override fun onDestroy() {  // todo also on hide this activity (onPause?)
@@ -92,9 +110,9 @@ 		val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
 		locationManager.removeUpdates(this)
 	}
 
-	private fun getItemsByQuery(query: String) {
+	private fun getItemsByQuery(server: Server, query: String) {
 		MainScope().launch {
-			val itemsStream = queryItems(query)
+			val itemsStream = queryItems(server, query)
 			if (itemsStream == null) {
 				// todo show empty state
 			} else {
@@ -104,10 +122,10 @@ 			Log.v("RESPONSE", "getItemsByQuery")
 		}
 	}
 
-	private fun getItemsByLocation(plusCode: String) {
+	private fun getItemsByLocation(server: Server, plusCode: String) {
 		Log.v("RESPONSE", "getting ItemsByLocation")
 		MainScope().launch {
-			val itemsStream = locateItems(plusCode)
+			val itemsStream = locateItems(server, plusCode)
 			if (itemsStream == null) {
 				// todo show empty state
 			} else {




diff --git a/app/src/main/res/layout/activity_feed_chooser.xml b/app/src/main/res/layout/activity_feed_chooser.xml
new file mode 100644
index 0000000000000000000000000000000000000000..95a30db5071a769fd056dba5d8a314522377c934
--- /dev/null
+++ b/app/src/main/res/layout/activity_feed_chooser.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- git:fixup feeds -->
+<androidx.constraintlayout.widget.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="match_parent"
+	tools:context=".feeds.FeedChooserActivity">
+
+	<com.google.android.material.textfield.TextInputLayout
+		android:id="@+id/server_field"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="16dp"
+		android:layout_marginTop="16dp"
+		android:layout_marginEnd="16dp"
+		android:hint="Bimba server"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toTopOf="parent">
+
+		<com.google.android.material.textfield.TextInputEditText
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content" />
+
+	</com.google.android.material.textfield.TextInputLayout>
+
+	<com.google.android.material.textfield.TextInputLayout
+		android:id="@+id/token_field"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="16dp"
+		android:layout_marginTop="16dp"
+		android:layout_marginEnd="16dp"
+		android:hint="Token"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@id/server_field">
+
+		<com.google.android.material.textfield.TextInputEditText
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content" />
+
+	</com.google.android.material.textfield.TextInputLayout>
+
+	<Button
+		android:id="@+id/button"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginTop="16dp"
+		android:text="Continue"
+		app:layout_constraintEnd_toEndOf="@+id/token_field"
+		app:layout_constraintStart_toStartOf="@+id/token_field"
+		app:layout_constraintTop_toBottomOf="@+id/token_field" />
+
+	<com.google.android.material.divider.MaterialDivider
+		android:id="@+id/divider"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_marginTop="16dp"
+		app:layout_constraintTop_toBottomOf="@+id/button" />
+
+	<com.google.android.material.progressindicator.CircularProgressIndicator
+		android:id="@+id/circularProgressIndicator"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:indeterminate="true"
+		android:visibility="gone"
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/divider" />
+
+	<TextView
+		android:id="@+id/feed_info"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="8dp"
+		android:layout_marginTop="16dp"
+		android:layout_marginEnd="8dp"
+		android:autoLink="web|email"
+		android:text=""
+		android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
+		android:visibility="gone"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/divider" />
+
+	<androidx.recyclerview.widget.RecyclerView
+		android:id="@+id/results_recycler"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="8dp"
+		android:layout_marginTop="8dp"
+		android:layout_marginEnd="8dp"
+		android:visibility="gone"
+		app:layout_behavior="@string/appbar_scrolling_view_behavior"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/feed_info" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4129ff7c6ffa1c1a9308dfc0872327e96d23ccee
--- /dev/null
+++ b/app/src/main/res/layout/feedinfo.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- git:fixup feeds -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	android:id="@+id/feedinfo"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content">
+
+	<TextView
+		android:id="@+id/feed_name"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="16dp"
+		android:layout_marginTop="16dp"
+		android:layout_marginBottom="16dp"
+		android:text=""
+		android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toTopOf="parent" />
+
+	<com.google.android.material.materialswitch.MaterialSwitch
+		android:id="@+id/feed_switch"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginTop="16dp"
+		android:layout_marginEnd="16dp"
+		android:layout_marginBottom="16dp"
+		android:text=""
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file