Skip to content

Commit 4a1aae1

Browse files
committed
feat: hub authentication UI with auth_keywords support
- HubConfigGson/ManagerData: add authKeywords field - GetterService/Impl: add managerUpdateHubAuth RPC - HubManager: add updateHubAuth() syncing Room + Rust - HubSettingDialog: refactor to BottomSheetDialogFragment with auth section - AuthEntryAdapter: new RecyclerView adapter for key-value auth editing - bottomsheet_hub_setting.xml / item_auth_entry.xml: new layouts - HubManagerActivity/ListItemHandler: use new BottomSheetDialogFragment - strings.xml: add hub_settings, authentication, auth_key_hint, auth_value_hint, add, save - Bump getter submodule pointer
1 parent 871a3ac commit 4a1aae1

File tree

13 files changed

+450
-48
lines changed

13 files changed

+450
-48
lines changed

app/src/main/java/net/xzos/upgradeall/ui/hubmanager/HubManagerActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ class HubManagerActivity : HubListActivity<HubManagerListItemView, HubManagerLis
1616

1717
override fun onOptionsItemSelected(item: MenuItem): Boolean {
1818
if (item.title == getString(R.string.global_setting)) {
19-
HubSettingDialog(null).show(this)
19+
HubSettingDialog.newInstance(null).show(supportFragmentManager, HubSettingDialog.TAG)
2020
}
2121
return super.onOptionsItemSelected(item)
2222
}
2323

2424
override val viewModel by viewModels<HubManagerViewModel>()
2525
override val adapter by lazy { HubManagerAdapter(HubManagerListItemHandler()) }
26-
}
26+
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package net.xzos.upgradeall.ui.hubmanager
22

33
import android.content.Context
4+
import androidx.appcompat.app.AppCompatActivity
45
import net.xzos.upgradeall.ui.base.recycleview.RecyclerViewHandler
56
import net.xzos.upgradeall.ui.hubmanager.setting.HubSettingDialog
67

78
class HubManagerListItemHandler : RecyclerViewHandler() {
89
fun onCardViewClick(context: Context, hubUuid: String) {
9-
HubSettingDialog(hubUuid).show(context)
10+
val fm = (context as AppCompatActivity).supportFragmentManager
11+
HubSettingDialog.newInstance(hubUuid).show(fm, HubSettingDialog.TAG)
1012
}
11-
}
13+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package net.xzos.upgradeall.ui.hubmanager.setting
2+
3+
import android.view.LayoutInflater
4+
import android.view.ViewGroup
5+
import android.widget.ArrayAdapter
6+
import androidx.recyclerview.widget.RecyclerView
7+
import net.xzos.upgradeall.databinding.ItemAuthEntryBinding
8+
9+
/**
10+
* RecyclerView adapter for editing hub authentication key-value pairs.
11+
*
12+
* @param entries Mutable list of (key, value) pairs to edit in-place.
13+
* @param keyHints Autocomplete suggestions for the key field (from hub.hubConfig.authKeywords).
14+
*/
15+
class AuthEntryAdapter(
16+
private val entries: MutableList<Pair<String, String>>,
17+
private val keyHints: List<String>,
18+
) : RecyclerView.Adapter<AuthEntryAdapter.ViewHolder>() {
19+
20+
inner class ViewHolder(private val binding: ItemAuthEntryBinding) :
21+
RecyclerView.ViewHolder(binding.root) {
22+
23+
fun bind(position: Int) {
24+
val (key, value) = entries[position]
25+
26+
// Populate autocomplete suggestions for the key field
27+
val context = binding.root.context
28+
val hintAdapter = ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line, keyHints)
29+
binding.authKeyEdit.setAdapter(hintAdapter)
30+
binding.authKeyEdit.setText(key)
31+
32+
// Set current value (avoid triggering password toggle animation)
33+
binding.authValueEdit.setText(value)
34+
35+
// Save edits back to the list on text change
36+
binding.authKeyEdit.setOnFocusChangeListener { _, _ ->
37+
entries[position] = binding.authKeyEdit.text.toString() to
38+
(binding.authValueEdit.text?.toString() ?: "")
39+
}
40+
binding.authValueEdit.setOnFocusChangeListener { _, _ ->
41+
entries[position] = (binding.authKeyEdit.text?.toString() ?: "") to
42+
(binding.authValueEdit.text?.toString() ?: "")
43+
}
44+
45+
binding.authDeleteButton.setOnClickListener {
46+
entries.removeAt(position)
47+
notifyItemRemoved(position)
48+
notifyItemRangeChanged(position, entries.size)
49+
}
50+
}
51+
}
52+
53+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
54+
val binding = ItemAuthEntryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
55+
return ViewHolder(binding)
56+
}
57+
58+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
59+
holder.bind(position)
60+
}
61+
62+
override fun getItemCount(): Int = entries.size
63+
64+
/** Flush currently focused text fields into the backing list before reading. */
65+
fun flushEntries() {
66+
// Focus changes trigger saves, but call this before collecting data to be safe.
67+
notifyDataSetChanged()
68+
}
69+
70+
/** Return a snapshot of current non-empty entries as a Map. */
71+
fun toAuthMap(): Map<String, String> =
72+
entries.filter { (k, _) -> k.isNotBlank() }.toMap()
73+
74+
/** Append a blank entry row. */
75+
fun addEntry() {
76+
entries.add("" to "")
77+
notifyItemInserted(entries.size - 1)
78+
}
79+
}
Lines changed: 115 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,133 @@
11
package net.xzos.upgradeall.ui.hubmanager.setting
22

3-
import android.content.Context
4-
import com.google.android.material.dialog.MaterialAlertDialogBuilder
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.lifecycle.lifecycleScope
8+
import androidx.recyclerview.widget.LinearLayoutManager
9+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
510
import kotlinx.coroutines.Dispatchers
6-
import kotlinx.coroutines.runBlocking
11+
import kotlinx.coroutines.launch
12+
import kotlinx.coroutines.withContext
713
import net.xzos.upgradeall.R
814
import net.xzos.upgradeall.core.database.table.extra_hub.ExtraHubEntityManager
915
import net.xzos.upgradeall.core.manager.HubManager
1016
import net.xzos.upgradeall.core.utils.URLReplaceData
11-
import net.xzos.upgradeall.databinding.DialogHubSettingBinding
12-
import net.xzos.upgradeall.utils.layoutInflater
17+
import net.xzos.upgradeall.databinding.BottomsheetHubSettingBinding
1318

14-
class HubSettingDialog(private val hubUuid: String? = null) {
15-
private val view by lazy { runBlocking(Dispatchers.Default) { getViewData() } }
19+
class HubSettingDialog : BottomSheetDialogFragment() {
1620

17-
fun show(context: Context) {
18-
hubUuid?.apply {
19-
HubManager.getHub(this)?.apply {
20-
showDialog(context)
21-
}
22-
} ?: showDialog(context)
21+
private var _binding: BottomsheetHubSettingBinding? = null
22+
private val binding get() = _binding!!
23+
24+
private var hubUuid: String? = null
25+
private lateinit var authAdapter: AuthEntryAdapter
26+
27+
override fun onCreate(savedInstanceState: Bundle?) {
28+
super.onCreate(savedInstanceState)
29+
hubUuid = arguments?.getString(ARG_HUB_UUID)
30+
}
31+
32+
override fun onCreateView(
33+
inflater: LayoutInflater,
34+
container: ViewGroup?,
35+
savedInstanceState: Bundle?,
36+
): View {
37+
_binding = BottomsheetHubSettingBinding.inflate(inflater, container, false)
38+
return binding.root
39+
}
40+
41+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
42+
super.onViewCreated(view, savedInstanceState)
43+
setupAuthSection()
44+
setupUrlReplaceSection()
45+
setupButtons()
2346
}
2447

25-
private suspend fun getViewData(): HubSettingView {
26-
val extraHubEntity = ExtraHubEntityManager.getExtraHub(null)
27-
val useGlobal = hubUuid?.let {
28-
extraHubEntity?.global ?: false
48+
private fun setupAuthSection() {
49+
val uuid = hubUuid
50+
if (uuid == null) {
51+
// Global settings: hide authentication section
52+
binding.authSection.visibility = View.GONE
53+
binding.authDivider.visibility = View.GONE
54+
return
2955
}
30-
return HubSettingView(
31-
useGlobal,
32-
extraHubEntity?.urlReplaceSearch,
33-
extraHubEntity?.urlReplaceString
34-
)
35-
}
36-
37-
private fun showDialog(context: Context) {
38-
val binding = DialogHubSettingBinding.inflate(context.layoutInflater)
39-
binding.item = view
40-
MaterialAlertDialogBuilder(context)
41-
.setView(binding.root)
42-
.setNegativeButton(context.getString(R.string.cancel)) { dialog, which ->
56+
57+
val hub = HubManager.getHub(uuid)
58+
val keyHints = hub?.hubConfig?.authKeywords ?: emptyList()
59+
val currentAuth = hub?.auth?.entries?.map { it.key to it.value }?.toMutableList()
60+
?: mutableListOf()
61+
62+
authAdapter = AuthEntryAdapter(currentAuth, keyHints)
63+
binding.authRecyclerView.apply {
64+
layoutManager = LinearLayoutManager(requireContext())
65+
adapter = authAdapter
66+
}
67+
68+
binding.authAddButton.setOnClickListener {
69+
authAdapter.addEntry()
70+
}
71+
}
72+
73+
private fun setupUrlReplaceSection() {
74+
lifecycleScope.launch(Dispatchers.IO) {
75+
val extraHubEntity = ExtraHubEntityManager.getExtraHub(hubUuid)
76+
withContext(Dispatchers.Main) {
77+
val useGlobal = extraHubEntity?.global ?: false
78+
binding.enableUrlReplaceSwitch.isChecked = useGlobal
79+
binding.matchRuleEdit.setText(extraHubEntity?.urlReplaceSearch ?: "")
80+
binding.replaceStringEdit.setText(extraHubEntity?.urlReplaceString ?: "")
81+
82+
// Only show the "use global" switch for per-hub dialogs
83+
binding.enableUrlReplaceSwitch.visibility =
84+
if (hubUuid != null) View.VISIBLE else View.GONE
4385
}
44-
.setPositiveButton(context.getString(R.string.ok)) { dialog, which ->
45-
val (useGlobal, URLReplaceData) = getURLReplaceData()
46-
runBlocking(Dispatchers.Default) {
47-
ExtraHubEntityManager.setUrlReplace(hubUuid, useGlobal, URLReplaceData)
86+
}
87+
}
88+
89+
private fun setupButtons() {
90+
binding.cancelButton.setOnClickListener { dismiss() }
91+
92+
binding.saveButton.setOnClickListener {
93+
val uuid = hubUuid
94+
val useGlobal = binding.enableUrlReplaceSwitch.isChecked
95+
val matchRule = binding.matchRuleEdit.text?.toString()
96+
val replaceString = binding.replaceStringEdit.text?.toString()
97+
98+
lifecycleScope.launch(Dispatchers.IO) {
99+
// Save URL replace rules
100+
ExtraHubEntityManager.setUrlReplace(
101+
uuid,
102+
useGlobal,
103+
URLReplaceData(matchRule, replaceString),
104+
)
105+
106+
// Save auth credentials (only for per-hub dialogs)
107+
if (uuid != null) {
108+
val authMap = authAdapter.toAuthMap()
109+
HubManager.updateHubAuth(uuid, authMap)
48110
}
111+
112+
withContext(Dispatchers.Main) { dismiss() }
49113
}
50-
.show()
114+
}
115+
}
116+
117+
override fun onDestroyView() {
118+
super.onDestroyView()
119+
_binding = null
51120
}
52121

53-
private fun getURLReplaceData(): Pair<Boolean, URLReplaceData> {
54-
return Pair(
55-
view.useGlobalSetting.get(), URLReplaceData(
56-
view.matchRule.get(),
57-
view.replaceString.get()
58-
)
59-
)
122+
companion object {
123+
private const val ARG_HUB_UUID = "hub_uuid"
124+
const val TAG = "HubSettingDialog"
125+
126+
fun newInstance(hubUuid: String? = null): HubSettingDialog =
127+
HubSettingDialog().apply {
128+
arguments = Bundle().apply {
129+
if (hubUuid != null) putString(ARG_HUB_UUID, hubUuid)
130+
}
131+
}
60132
}
61-
}
133+
}

0 commit comments

Comments
 (0)