Skip to content

Commit bac8071

Browse files
committed
feat: rebuild custom instances dialogs for better UX
1 parent 71f0d48 commit bac8071

17 files changed

Lines changed: 276 additions & 84 deletions

app/src/main/java/com/github/libretube/constants/IntentData.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ object IntentData {
5959
const val segments = "segments"
6060
const val alreadyStarted = "alreadyStarted"
6161
const val showUpcoming = "showUpcoming"
62+
const val customInstance = "customInstance"
6263
}

app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ object PreferenceKeys {
4040
const val AUTH_INSTANCE = "selectAuthInstance"
4141
const val AUTH_INSTANCE_TOGGLE = "auth_instance_toggle"
4242
const val CUSTOM_INSTANCE = "customInstance"
43-
const val CLEAR_CUSTOM_INSTANCES = "clearCustomInstances"
4443
const val LOGIN_REGISTER = "login_register"
4544
const val LOGOUT = "logout"
4645
const val DELETE_ACCOUNT = "delete_account"
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
11
package com.github.libretube.db.dao
22

33
import androidx.room.Dao
4+
import androidx.room.Delete
45
import androidx.room.Insert
56
import androidx.room.OnConflictStrategy
67
import androidx.room.Query
78
import com.github.libretube.db.obj.CustomInstance
9+
import kotlinx.coroutines.flow.Flow
810

911
@Dao
1012
interface CustomInstanceDao {
11-
@Query("SELECT * FROM customInstance")
13+
@Query("SELECT * FROM customInstance ORDER BY name")
1214
suspend fun getAll(): List<CustomInstance>
1315

16+
@Query("SELECT * FROM customInstance ORDER BY name")
17+
fun getAllFlow(): Flow<List<CustomInstance>>
18+
1419
@Insert(onConflict = OnConflictStrategy.REPLACE)
1520
suspend fun insert(customInstance: CustomInstance)
1621

1722
@Insert(onConflict = OnConflictStrategy.REPLACE)
1823
suspend fun insertAll(customInstances: List<CustomInstance>)
1924

25+
@Query("SELECT * FROM customInstance WHERE apiUrl = :apiUrl")
26+
suspend fun getByApiUrl(apiUrl: String): CustomInstance?
27+
28+
@Delete
29+
suspend fun deleteCustomInstance(customInstance: CustomInstance)
30+
2031
@Query("DELETE FROM customInstance")
2132
suspend fun deleteAll()
2233
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package com.github.libretube.db.obj
22

3+
import android.os.Parcelable
34
import androidx.room.ColumnInfo
45
import androidx.room.Entity
56
import androidx.room.PrimaryKey
7+
import kotlinx.parcelize.Parcelize
68
import kotlinx.serialization.Serializable
79

810
@Serializable
911
@Entity(tableName = "customInstance")
12+
@Parcelize
1013
class CustomInstance(
1114
@PrimaryKey var name: String = "",
1215
@ColumnInfo var apiUrl: String = "",
1316
@ColumnInfo var frontendUrl: String = ""
14-
)
17+
) : Parcelable
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.github.libretube.ui.adapters
2+
3+
import android.view.LayoutInflater
4+
import android.view.ViewGroup
5+
import androidx.recyclerview.widget.ListAdapter
6+
import com.github.libretube.databinding.CustomInstanceRowBinding
7+
import com.github.libretube.db.obj.CustomInstance
8+
import com.github.libretube.ui.adapters.callbacks.DiffUtilItemCallback
9+
import com.github.libretube.ui.viewholders.CustomInstancesViewHolder
10+
11+
class CustomInstancesAdapter(
12+
private val onClickInstance: (CustomInstance) -> Unit,
13+
private val onDeleteInstance: (CustomInstance) -> Unit
14+
) : ListAdapter<CustomInstance, CustomInstancesViewHolder>(
15+
DiffUtilItemCallback()
16+
) {
17+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomInstancesViewHolder {
18+
val layoutInflater = LayoutInflater.from(parent.context)
19+
val binding = CustomInstanceRowBinding.inflate(layoutInflater, parent, false)
20+
return CustomInstancesViewHolder(binding)
21+
}
22+
23+
override fun onBindViewHolder(holder: CustomInstancesViewHolder, position: Int) {
24+
val instance = getItem(position)!!
25+
26+
with (holder.binding) {
27+
instanceName.text = instance.name
28+
29+
root.setOnClickListener {
30+
onClickInstance(instance)
31+
}
32+
33+
deleteInstance.setOnClickListener {
34+
onDeleteInstance.invoke(instance)
35+
}
36+
}
37+
}
38+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.github.libretube.ui.dialogs
2+
3+
import android.app.Dialog
4+
import android.content.DialogInterface
5+
import android.os.Bundle
6+
import androidx.fragment.app.DialogFragment
7+
import androidx.fragment.app.activityViewModels
8+
import com.github.libretube.R
9+
import com.github.libretube.constants.IntentData
10+
import com.github.libretube.databinding.DialogCustomInstanceBinding
11+
import com.github.libretube.db.obj.CustomInstance
12+
import com.github.libretube.extensions.parcelable
13+
import com.github.libretube.extensions.toastFromMainThread
14+
import com.github.libretube.ui.models.CustomInstancesModel
15+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
16+
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
17+
import java.net.MalformedURLException
18+
19+
class CreateCustomInstanceDialog : DialogFragment() {
20+
val viewModel: CustomInstancesModel by activityViewModels()
21+
22+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
23+
val binding = DialogCustomInstanceBinding.inflate(layoutInflater)
24+
arguments?.parcelable<CustomInstance>(IntentData.customInstance)?.let { initialInstance ->
25+
binding.instanceName.setText(initialInstance.name)
26+
binding.instanceApiUrl.setText(initialInstance.apiUrl)
27+
binding.instanceFrontendUrl.setText(initialInstance.frontendUrl)
28+
}
29+
30+
binding.instanceApiUrl.setOnFocusChangeListener { _, hasFocus ->
31+
if (hasFocus || !binding.instanceName.text.isNullOrEmpty()) return@setOnFocusChangeListener
32+
33+
// automatically set the api name
34+
val apiUrl = binding.instanceApiUrl.text.toString().toHttpUrlOrNull()
35+
if (apiUrl != null) {
36+
binding.instanceName.setText(apiUrl.host)
37+
}
38+
}
39+
40+
return MaterialAlertDialogBuilder(requireContext())
41+
.setTitle(R.string.customInstance)
42+
.setView(binding.root)
43+
.setPositiveButton(R.string.addInstance, null)
44+
.setNegativeButton(R.string.cancel, null)
45+
.show()
46+
.apply {
47+
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
48+
val instanceName = binding.instanceName.text.toString()
49+
val apiUrl = binding.instanceApiUrl.text.toString()
50+
val frontendUrl = binding.instanceFrontendUrl.text.toString()
51+
52+
try {
53+
viewModel.addCustomInstance(apiUrl, instanceName, frontendUrl)
54+
requireDialog().dismiss()
55+
} catch (e: IllegalArgumentException) {
56+
context.toastFromMainThread(R.string.empty_instance)
57+
} catch (e: MalformedURLException) {
58+
context.toastFromMainThread(R.string.invalid_url)
59+
}
60+
}
61+
}
62+
}
63+
}

app/src/main/java/com/github/libretube/ui/dialogs/CustomInstanceDialog.kt

Lines changed: 0 additions & 53 deletions
This file was deleted.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.github.libretube.ui.dialogs
2+
3+
import android.app.Dialog
4+
import android.content.DialogInterface
5+
import android.os.Bundle
6+
import androidx.core.os.bundleOf
7+
import androidx.fragment.app.DialogFragment
8+
import androidx.fragment.app.activityViewModels
9+
import androidx.lifecycle.lifecycleScope
10+
import com.github.libretube.R
11+
import com.github.libretube.constants.IntentData
12+
import com.github.libretube.databinding.DialogCustomIntancesListBinding
13+
import com.github.libretube.ui.adapters.CustomInstancesAdapter
14+
import com.github.libretube.ui.models.CustomInstancesModel
15+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
16+
import kotlinx.coroutines.flow.collectLatest
17+
import kotlinx.coroutines.launch
18+
19+
class CustomInstancesListDialog: DialogFragment() {
20+
val viewModel: CustomInstancesModel by activityViewModels()
21+
22+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
23+
val binding = DialogCustomIntancesListBinding.inflate(layoutInflater)
24+
val adapter = CustomInstancesAdapter(
25+
onClickInstance = {
26+
CreateCustomInstanceDialog()
27+
.apply {
28+
arguments = bundleOf(IntentData.customInstance to it)
29+
}
30+
.show(childFragmentManager, null)
31+
},
32+
onDeleteInstance = {
33+
viewModel.deleteCustomInstance(it)
34+
}
35+
)
36+
binding.customInstancesRecycler.adapter = adapter
37+
38+
lifecycleScope.launch {
39+
viewModel.instances.collectLatest {
40+
adapter.submitList(it)
41+
}
42+
}
43+
44+
return MaterialAlertDialogBuilder(requireContext())
45+
.setTitle(getString(R.string.customInstance))
46+
.setView(binding.root)
47+
.setPositiveButton(getString(R.string.okay), null)
48+
.setNegativeButton(getString(R.string.addInstance), null)
49+
.show()
50+
.apply {
51+
getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener {
52+
CreateCustomInstanceDialog()
53+
.show(childFragmentManager, null)
54+
}
55+
}
56+
}
57+
}

app/src/main/java/com/github/libretube/ui/dialogs/ShareDialog.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,12 @@ class ShareDialog : DialogFragment() {
118118
)
119119

120120
// get the api urls of the other custom instances
121-
val customInstances = runBlocking(Dispatchers.IO) {
122-
Database.customInstanceDao().getAll()
121+
val customInstance = runBlocking(Dispatchers.IO) {
122+
Database.customInstanceDao().getByApiUrl(instancePref)
123123
}
124124

125125
// return the custom instance frontend url if available
126-
return customInstances.firstOrNull { it.apiUrl == instancePref }?.frontendUrl.orEmpty()
126+
return customInstance?.frontendUrl.orEmpty()
127127
}
128128

129129
private fun generateLinkText(binding: DialogShareBinding, customInstanceUrl: HttpUrl?): String {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.github.libretube.ui.models
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
5+
import com.github.libretube.db.DatabaseHolder.Database
6+
import com.github.libretube.db.obj.CustomInstance
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.flow.flowOn
9+
import kotlinx.coroutines.launch
10+
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
11+
import java.net.MalformedURLException
12+
13+
class CustomInstancesModel: ViewModel() {
14+
val instances = Database.customInstanceDao().getAllFlow()
15+
.flowOn(Dispatchers.IO)
16+
17+
fun addCustomInstance(apiUrlInput: String, instanceNameInput: String?, frontendUrlInput: String?) {
18+
if (apiUrlInput.isEmpty()) throw IllegalArgumentException()
19+
20+
val apiUrl = apiUrlInput.toHttpUrlOrNull() ?: throw MalformedURLException()
21+
val frontendUrl = if (!frontendUrlInput.isNullOrBlank()) {
22+
frontendUrlInput.toHttpUrlOrNull() ?: throw MalformedURLException()
23+
} else {
24+
null
25+
}
26+
27+
viewModelScope.launch(Dispatchers.IO) {
28+
val instanceName = instanceNameInput ?: apiUrl.host
29+
30+
Database.customInstanceDao()
31+
.insert(CustomInstance(instanceName, apiUrl.toString(), frontendUrl?.toString().orEmpty()))
32+
}
33+
}
34+
35+
fun deleteCustomInstance(customInstance: CustomInstance) = viewModelScope.launch(Dispatchers.IO) {
36+
Database.customInstanceDao().deleteCustomInstance(customInstance)
37+
}
38+
}

0 commit comments

Comments
 (0)