Author: Adam <git@apiote.xyz>
split onboarding into simple/advanced and server then feeds
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6e86b57b4ced88504298a3b09380d33dd243e046..9df27e64cd94320f13c1b8e8bf9a32115899eaa9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,10 +19,20 @@ android:supportsRtl="true" android:theme="@style/Theme.Bimba.Style" tools:targetApi="31"> <activity - android:name=".feeds.FeedChooserActivity" + android:name=".settings.ServerChooserActivity" + android:exported="false"> + <meta-data + android:name="android.app.lib_name" + android:value="" /> + </activity> + <activity + android:name=".onboarding.OnboardingActivity"> + </activity> + <activity + android:name=".settings.feeds.FeedChooserActivity" android:exported="false" /> <activity - android:name=".FirstRunActivity" + android:name=".onboarding.FirstRunActivity" android:exported="true" android:theme="@style/Theme.Bimba.Splash"> <intent-filter> diff --git a/app/src/main/java/ml/adamsprogs/bimba/FirstRunActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/FirstRunActivity.kt deleted file mode 100644 index 7cd423c91d4152df677f57e69b27d07bb473d234..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/FirstRunActivity.kt +++ /dev/null @@ -1,24 +0,0 @@ -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/Structs.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt index 99a5abc5900a4e81e2955d885f3eb2ae76032a02..b074d8b82c53f55f1ccadf3ae79987693d583418 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt @@ -5,6 +5,7 @@ import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable +import android.text.format.DateFormat import android.text.format.DateUtils import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.ColorUtils.HSLToColor @@ -51,6 +52,10 @@ val offsetSign: Int, val offsetH: Int, val offsetM: Int ) { + fun toString(context: Context): String { + return DateFormat.getDateFormat(context).format(Date(year, month, day)) + } + companion object { fun of(s: String): DateTime { val (oH, oM) = if (s[19] == 'Z') { diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/MainActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/MainActivity.kt index 676af3a60b6b9d8c5d09ea3ab0c679246f640f5d..8a6042c6b82b03ac27a0bcba0dcf901bfbe9fca1 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/MainActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +import androidx.core.content.edit import androidx.core.view.WindowCompat import androidx.core.view.get import androidx.fragment.app.Fragment @@ -40,6 +41,10 @@ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + + getSharedPreferences("shp", MODE_PRIVATE).edit(true) { + putBoolean("firstRun", false) + } supportFragmentManager.registerFragmentLifecycleCallbacks( object : FragmentLifecycleCallbacks() { diff --git a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt deleted file mode 100644 index 7b31fa6a0eaff1d039aa770a45e1cca311315c06..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt +++ /dev/null @@ -1,148 +0,0 @@ -package ml.adamsprogs.bimba.feeds - -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.net.ConnectivityManager -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle -import android.util.Log -import android.view.View -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 - -// todo split into server+token -> dialog if token not given and (rate limited or access denied), [3.1] check .well-known -> feeds choosing - - -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 with attribution and info - } - 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.progress.visibility = View.VISIBLE - binding.resultsRecycler.visibility = View.GONE - binding.feedInfo.visibility = View.GONE - - MainScope().launch { - val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val feedsResult = getFeeds(cm, Server(host, token, "")) - val response = if (feedsResult.stream != null) { - unmarshallFeedsResponse(feedsResult.stream) - } else { - null - } - if (feedsResult.error != null) { - binding.feedInfo.text = getString(feedsResult.error.stringResource) - Log.w("FeedChooser", "$feedsResult") - Log.w("FeedChooser", "$response") - } else { - updateItems(response as FeedsSuccess) // todo(error-handling) handle parsing error (not bimba server, wrong API version) - 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) - putString("server", binding.serverField.editText!!.text.toString()) - putString("token", binding.tokenField.editText!!.text.toString()) - } - if (wasFirstRun) { - finish() - } - } - - private suspend fun unmarshallFeedsResponse(stream: InputStream): FeedsResponse { - return withContext(Dispatchers.IO) { - FeedsResponse.unmarshal(stream) - } - } - - private fun updateItems(response: FeedsSuccess) { - binding.progress.visibility = View.GONE - binding.resultsRecycler.visibility = View.VISIBLE - binding.feedInfo.visibility = View.VISIBLE - binding.feedInfo.text = // todo(ui) remove after splitting - 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 deleted file mode 100644 index 34a1d1f84653bd4fdcc920d056a371d3f6d75842..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedInfos.kt +++ /dev/null @@ -1,79 +0,0 @@ -package ml.adamsprogs.bimba.feeds - -import android.annotation.SuppressLint -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.feed) - 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 - - @SuppressLint("NotifyDataSetChanged") // todo [3.1] DiffUtil - fun update(items: List<FeedInfo>) { - feeds = items - notifyDataSetChanged() - } - -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/onboarding/FirstRunActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/onboarding/FirstRunActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..b289c3268b2254d98093af0186d65d5613160f80 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/onboarding/FirstRunActivity.kt @@ -0,0 +1,23 @@ +package ml.adamsprogs.bimba.onboarding + +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 + +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, OnboardingActivity::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/onboarding/OnboardingActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/onboarding/OnboardingActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..f49ba2470f879f08cb2bdea516719d1aa3f83565 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/onboarding/OnboardingActivity.kt @@ -0,0 +1,73 @@ +package ml.adamsprogs.bimba.onboarding + +import android.content.Intent +import android.graphics.Typeface +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.RelativeSizeSpan +import android.text.style.StyleSpan +import android.widget.Button +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import ml.adamsprogs.bimba.R +import ml.adamsprogs.bimba.databinding.ActivityOnboardingBinding +import ml.adamsprogs.bimba.settings.ServerChooserActivity + +class OnboardingActivity : AppCompatActivity() { + private var _binding: ActivityOnboardingBinding? = null + private val binding get() = _binding!! + + private val activityLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (!getSharedPreferences("shp", MODE_PRIVATE).getBoolean("firstRun", true)) { + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + _binding = ActivityOnboardingBinding.inflate(layoutInflater) + setContentView(binding.root) + + + prepareButton( + binding.buttonSimple, + getString(R.string.onboarding_simple), + getString(R.string.onboarding_simple_action), + true + ) + prepareButton( + binding.buttonAdvanced, + getString(R.string.onboarding_advanced), + getString(R.string.onboarding_advanced_action), + false + ) + } + + private fun prepareButton(button: Button, title: String, description: String, simple: Boolean) { + button.text = SpannableStringBuilder().apply { + append( + title, + StyleSpan(Typeface.BOLD), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + append("\n") + append( + description, + RelativeSizeSpan(.75f), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + button.setOnClickListener { + moveOn(simple) + } + } + + private fun moveOn(simple: Boolean) { + val intent = Intent(this, ServerChooserActivity::class.java).apply { + putExtra("simple", simple) + } + activityLauncher.launch(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/settings/ServerChooserActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/settings/ServerChooserActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..9b66339ee929d6672d077cfdf83188b16f1e833b --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/settings/ServerChooserActivity.kt @@ -0,0 +1,137 @@ +package ml.adamsprogs.bimba.settings + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.ConnectivityManager +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.edit +import androidx.core.widget.addTextChangedListener +import com.google.android.material.dialog.MaterialAlertDialogBuilder +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.databinding.ActivityServerChooserBinding +import ml.adamsprogs.bimba.settings.feeds.FeedChooserActivity + +class ServerChooserActivity : AppCompatActivity() { + private var _binding: ActivityServerChooserBinding? = null + private val binding get() = _binding!! + + private val activityLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (!preferences.getBoolean("inFeedsTransaction", true)) { + finish() + } + } + + private lateinit var preferences: SharedPreferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + preferences = getSharedPreferences("shp", MODE_PRIVATE) + + if (intent.getBooleanExtra("simple", false)) { + setServer("bimba.apiote.xyz", "") + runFeedsActivity() + finish() + } + + _binding = ActivityServerChooserBinding.inflate(layoutInflater) + setContentView(binding.root) + + preferences.edit(true){ + putBoolean("inFeedsTransaction", true) + } + + binding.button.isEnabled = false + binding.serverField.editText!!.addTextChangedListener { editable -> + binding.button.isEnabled = !editable.isNullOrBlank() + } + + binding.button.setOnClickListener { + setServer( + binding.serverField.editText!!.text.toString(), + binding.tokenField.editText!!.text.toString() + ) + checkServer() + } + } + + private fun showDialog( + title: Int, + description: Int, + icon: Int, + onPositive: (() -> Unit)? + ) { + MaterialAlertDialogBuilder(this) + .setIcon(AppCompatResources.getDrawable(this, icon)) + .setTitle(getString(title)) + .setMessage(getString(description)) + .setNegativeButton(resources.getString(R.string.cancel)) { _, _ -> }.apply { + if (onPositive != null) { + setPositiveButton(resources.getString(R.string.cont)) { _, _ -> + onPositive() + } + } + } + .show() + } + + private fun checkServer() { + // todo [api-freeze] check is really bimba (/.well-known/bimba) + MainScope().launch { + val result = getFeeds( + getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager, + Server.get(this@ServerChooserActivity) + ) + if (result.error != null) { + Log.w("ServerChooser", "$result") + showDialog(R.string.error, result.error.stringResource, result.error.imageResource, null) + return@launch + } + + val response = withContext(Dispatchers.IO) { + FeedsResponse.unmarshal(result.stream!!) + } + if (response is FeedsSuccess) { + val token = preferences.getString("token", "") + if (response.rateLimited && token == "") { + showDialog(R.string.rate_limit, R.string.server_rate_limited_question, R.drawable.error_limit) { + runFeedsActivity() + } + return@launch + } + if (response.private && token == "") { + showDialog(R.string.error, R.string.server_private_question, R.drawable.error_sec, null) + return@launch + } + runFeedsActivity() + } else { + // todo error handling + // todo [api-freeze] check api version + } + } + } + + private fun setServer(hostname: String, token: String) { + preferences.edit(true) { + putString("host", hostname) + putString("token", token) + } + } + + private fun runFeedsActivity() { + activityLauncher.launch(Intent(this, FeedChooserActivity::class.java)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/settings/feeds/FeedChooserActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/settings/feeds/FeedChooserActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3527ffcc914b5ed7fb8e9cdd1989b7218849aad --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/settings/feeds/FeedChooserActivity.kt @@ -0,0 +1,89 @@ +package ml.adamsprogs.bimba.settings.feeds + +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.core.content.edit +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.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 + +class FeedChooserActivity : AppCompatActivity() { + private var _binding: ActivityFeedChooserBinding? = null + private val binding get() = _binding!! + + private lateinit var adapter: BimbaFeedInfoAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + _binding = ActivityFeedChooserBinding.inflate(layoutInflater) + setContentView(binding.root) + + setUpRecycler() + getServer() + + binding.button.setOnClickListener { + moveOn() + } + } + + private fun setUpRecycler() { + binding.resultsRecycler.layoutManager = LinearLayoutManager(this) + adapter = BimbaFeedInfoAdapter(layoutInflater, listOf(), this) { + FeedBottomSheet(it).show(supportFragmentManager, FeedBottomSheet.TAG) + } + binding.resultsRecycler.adapter = adapter + } + + private fun getServer() { + binding.progress.visibility = View.VISIBLE + binding.resultsRecycler.visibility = View.GONE + + MainScope().launch { + val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val feedsResult = getFeeds(cm, Server.get(this@FeedChooserActivity)) + if (feedsResult.error != null) { + Log.w("FeedChooser", "$feedsResult") + return@launch + } + val response = withContext(Dispatchers.IO) { + FeedsResponse.unmarshal(feedsResult.stream!!) + } + if (response is FeedsSuccess) { + updateItems(response) + } else { + // todo error handling + } + } + } + + private fun moveOn() { + val preferences = getSharedPreferences("shp", MODE_PRIVATE) + preferences.edit(true) { + putBoolean("inFeedsTransaction", false) + } + if (preferences.getBoolean("firstRun", true)) { + val intent = Intent(this, MainActivity::class.java) + startActivity(intent) + } + finish() + } + + private fun updateItems(response: FeedsSuccess) { + binding.progress.visibility = View.GONE + binding.resultsRecycler.visibility = View.VISIBLE + adapter.update(response.feeds) + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/settings/feeds/FeedInfos.kt b/app/src/main/java/ml/adamsprogs/bimba/settings/feeds/FeedInfos.kt new file mode 100644 index 0000000000000000000000000000000000000000..380ad2a4060a5ab92be796789ebacc837ea14be1 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/settings/feeds/FeedInfos.kt @@ -0,0 +1,107 @@ +package ml.adamsprogs.bimba.settings.feeds + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.os.Bundle +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.bottomsheet.BottomSheetDialogFragment +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.feed) + 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 + + @SuppressLint("NotifyDataSetChanged") // todo [3.1] DiffUtil + fun update(items: List<FeedInfo>) { + feeds = items + notifyDataSetChanged() + } +} + +class FeedBottomSheet(private var feed: FeedInfo) : BottomSheetDialogFragment() { + companion object { + const val TAG = "DepartureBottomSheet" + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val content = inflater.inflate(R.layout.feed_bottom_sheet, container, false) + content.findViewById<TextView>(R.id.title).text = feed.name + content.findViewById<TextView>(R.id.description).text = feed.description + content.findViewById<TextView>(R.id.attribution).text = feed.attribution + content.findViewById<TextView>(R.id.update_time).text = + getString(R.string.last_update, if (context != null) { + feed.lastUpdate.toString(requireContext()) + } else { + feed.lastUpdate.let { "${it.year}-${it.month}-${it.day}" } + }) + return content + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_feed_chooser.xml b/app/src/main/res/layout/activity_feed_chooser.xml index 8d8a85d5706a3ddc0ecc787289988d9ffc1ead1f..7a6ba5a4d211a0a4e4760be337cd55b20e930879 100644 --- a/app/src/main/res/layout/activity_feed_chooser.xml +++ b/app/src/main/res/layout/activity_feed_chooser.xml @@ -4,60 +4,7 @@ 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="@string/bimba_server_address_hint" - 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="@string/bimba_server_token_hint" - 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="@string/bimba_server_continue_button" - 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" /> + tools:context=".settings.feeds.FeedChooserActivity"> <com.google.android.material.progressindicator.CircularProgressIndicator android:id="@+id/progress" @@ -68,33 +15,29 @@ android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/divider" /> - - <com.google.android.material.textview.MaterialTextView - 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" /> + app:layout_constraintTop_toTopOf="parent" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/results_recycler" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="0dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" - android:visibility="gone" + android:layout_marginBottom="8dp" app:layout_behavior="@string/appbar_scrolling_view_behavior" + app:layout_constraintBottom_toTopOf="@+id/button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/feed_info" /> + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:layout_marginBottom="16dp" + android:text="@string/save" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml new file mode 100644 index 0000000000000000000000000000000000000000..15b97bbb6b05d7328e0d71068d0d5c6fb412d81d --- /dev/null +++ b/app/src/main/res/layout/activity_onboarding.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<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=".onboarding.OnboardingActivity"> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent=".125" /> + + <com.google.android.material.textview.MaterialTextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:text="@string/onboarding_question" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/guideline2" /> + + <Button + android:id="@+id/button_simple" + style="?attr/materialButtonOutlinedStyle" + android:layout_width="200dp" + android:layout_height="100dp" + android:layout_marginBottom="16dp" + app:cornerRadius="10dp" + app:layout_constraintBottom_toTopOf="@+id/guideline" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent=".5" /> + + <Button + android:id="@+id/button_advanced" + style="?attr/materialButtonOutlinedStyle" + android:layout_width="200dp" + android:layout_height="100dp" + android:layout_marginTop="16dp" + app:cornerRadius="10dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/guideline" /> + + <com.google.android.material.textview.MaterialTextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="16dp" + android:text="@string/seatbelts_everyone" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.BodySmall" + android:textStyle="italic" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_server_chooser.xml b/app/src/main/res/layout/activity_server_chooser.xml new file mode 100644 index 0000000000000000000000000000000000000000..125c84dbbf76f24c4616ccd63ffd235340216fb6 --- /dev/null +++ b/app/src/main/res/layout/activity_server_chooser.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<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=".settings.ServerChooserActivity"> + + <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="@string/bimba_server_address_hint" + 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="@string/bimba_server_token_hint" + 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="@string/bimba_server_continue_button" + app:layout_constraintEnd_toEndOf="@+id/token_field" + app:layout_constraintStart_toStartOf="@+id/token_field" + app:layout_constraintTop_toBottomOf="@+id/token_field" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/feed_bottom_sheet.xml b/app/src/main/res/layout/feed_bottom_sheet.xml new file mode 100644 index 0000000000000000000000000000000000000000..41e7751df918a60fbb5f4f62925ae1705581ca86 --- /dev/null +++ b/app/src/main/res/layout/feed_bottom_sheet.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tool="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="16dp"> + + <com.google.android.material.bottomsheet.BottomSheetDragHandleView + android:id="@+id/drag_handle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="48dp" + android:layout_marginEnd="8dp" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.DisplaySmall" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tool:text="Poznań ZTM" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.BodyLarge" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/title" + tool:text="Feed for Poznań" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/attribution" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.BodyMedium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/description" + tool:text="(c) Poznań" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/update_time" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.BodyMedium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/attribution" + tool:text="Last update: 2023-01-01" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ 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 c20bcf31f1e57cb276fd5864b46b8265e8dcfe62..5195ff82dbe7883a66e34b3aeaa4a6ef63888584 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,10 +69,16 @@USB charging <string name="show_departures">Show departures</string> <string name="open_in_maps_app">Open in maps app</string> <string name="stop_content_description">stop</string> - <string name="seatbelts_everyone">Seatbelts, everyone!</string> + <string name="seatbelts_everyone">Seatbelts, everyone!</string> <!-- taken from ‘Magic School Bus’. Should be translated like in the series --> <string name="onboarding_question">How would you like to start?</string> <string name="onboarding_simple">Simple</string> <string name="onboarding_simple_action">choose cities</string> <string name="onboarding_advanced">Advanced</string> - <string name="onboarding_simple_advanced">choose server</string> <!-- taken from ‘Magic School Bus’. Should be translated like in the series --> + <string name="onboarding_advanced_action">choose server</string> + <string name="cancel">Cancel</string> + <string name="error">Error</string> + <string name="rate_limit">Server is rate-limited</string> + <string name="server_rate_limited_question">This server is rate-limited and no token was given. Do you want to continue?</string> + <string name="server_private_question">This server is private and no token was given</string> + <string name="last_update">Last update: %s</string> </resources> \ No newline at end of file