mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 15:30:31 +05:30
Merge pull request #7166 from Bnyro/master
feat: rebuild custom instances dialogs for better UX
This commit is contained in:
commit
0a81c50c4b
@ -59,4 +59,5 @@ object IntentData {
|
||||
const val segments = "segments"
|
||||
const val alreadyStarted = "alreadyStarted"
|
||||
const val showUpcoming = "showUpcoming"
|
||||
const val customInstance = "customInstance"
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ object PreferenceKeys {
|
||||
const val AUTH_INSTANCE = "selectAuthInstance"
|
||||
const val AUTH_INSTANCE_TOGGLE = "auth_instance_toggle"
|
||||
const val CUSTOM_INSTANCE = "customInstance"
|
||||
const val CLEAR_CUSTOM_INSTANCES = "clearCustomInstances"
|
||||
const val LOGIN_REGISTER = "login_register"
|
||||
const val LOGOUT = "logout"
|
||||
const val DELETE_ACCOUNT = "delete_account"
|
||||
|
@ -1,22 +1,33 @@
|
||||
package com.github.libretube.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.github.libretube.db.obj.CustomInstance
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface CustomInstanceDao {
|
||||
@Query("SELECT * FROM customInstance")
|
||||
@Query("SELECT * FROM customInstance ORDER BY name")
|
||||
suspend fun getAll(): List<CustomInstance>
|
||||
|
||||
@Query("SELECT * FROM customInstance ORDER BY name")
|
||||
fun getAllFlow(): Flow<List<CustomInstance>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(customInstance: CustomInstance)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(customInstances: List<CustomInstance>)
|
||||
|
||||
@Query("SELECT * FROM customInstance WHERE apiUrl = :apiUrl")
|
||||
suspend fun getByApiUrl(apiUrl: String): CustomInstance?
|
||||
|
||||
@Delete
|
||||
suspend fun deleteCustomInstance(customInstance: CustomInstance)
|
||||
|
||||
@Query("DELETE FROM customInstance")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
package com.github.libretube.db.obj
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "customInstance")
|
||||
@Parcelize
|
||||
class CustomInstance(
|
||||
@PrimaryKey var name: String = "",
|
||||
@ColumnInfo var apiUrl: String = "",
|
||||
@ColumnInfo var frontendUrl: String = ""
|
||||
)
|
||||
) : Parcelable
|
||||
|
@ -0,0 +1,38 @@
|
||||
package com.github.libretube.ui.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import com.github.libretube.databinding.CustomInstanceRowBinding
|
||||
import com.github.libretube.db.obj.CustomInstance
|
||||
import com.github.libretube.ui.adapters.callbacks.DiffUtilItemCallback
|
||||
import com.github.libretube.ui.viewholders.CustomInstancesViewHolder
|
||||
|
||||
class CustomInstancesAdapter(
|
||||
private val onClickInstance: (CustomInstance) -> Unit,
|
||||
private val onDeleteInstance: (CustomInstance) -> Unit
|
||||
) : ListAdapter<CustomInstance, CustomInstancesViewHolder>(
|
||||
DiffUtilItemCallback()
|
||||
) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomInstancesViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
val binding = CustomInstanceRowBinding.inflate(layoutInflater, parent, false)
|
||||
return CustomInstancesViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: CustomInstancesViewHolder, position: Int) {
|
||||
val instance = getItem(position)!!
|
||||
|
||||
with (holder.binding) {
|
||||
instanceName.text = instance.name
|
||||
|
||||
root.setOnClickListener {
|
||||
onClickInstance(instance)
|
||||
}
|
||||
|
||||
deleteInstance.setOnClickListener {
|
||||
onDeleteInstance.invoke(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package com.github.libretube.ui.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.databinding.DialogCustomInstanceBinding
|
||||
import com.github.libretube.db.obj.CustomInstance
|
||||
import com.github.libretube.extensions.parcelable
|
||||
import com.github.libretube.extensions.toastFromMainThread
|
||||
import com.github.libretube.ui.models.CustomInstancesModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import java.net.MalformedURLException
|
||||
|
||||
class CreateCustomInstanceDialog : DialogFragment() {
|
||||
val viewModel: CustomInstancesModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = DialogCustomInstanceBinding.inflate(layoutInflater)
|
||||
arguments?.parcelable<CustomInstance>(IntentData.customInstance)?.let { initialInstance ->
|
||||
binding.instanceName.setText(initialInstance.name)
|
||||
binding.instanceApiUrl.setText(initialInstance.apiUrl)
|
||||
binding.instanceFrontendUrl.setText(initialInstance.frontendUrl)
|
||||
}
|
||||
|
||||
binding.instanceApiUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus || !binding.instanceName.text.isNullOrEmpty()) return@setOnFocusChangeListener
|
||||
|
||||
// automatically set the api name
|
||||
val apiUrl = binding.instanceApiUrl.text.toString().toHttpUrlOrNull()
|
||||
if (apiUrl != null) {
|
||||
binding.instanceName.setText(apiUrl.host)
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.customInstance)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.addInstance, null)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
.apply {
|
||||
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
|
||||
val instanceName = binding.instanceName.text.toString()
|
||||
val apiUrl = binding.instanceApiUrl.text.toString()
|
||||
val frontendUrl = binding.instanceFrontendUrl.text.toString()
|
||||
|
||||
try {
|
||||
viewModel.addCustomInstance(apiUrl, instanceName, frontendUrl)
|
||||
requireDialog().dismiss()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
context.toastFromMainThread(R.string.empty_instance)
|
||||
} catch (e: MalformedURLException) {
|
||||
context.toastFromMainThread(R.string.invalid_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package com.github.libretube.ui.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.databinding.DialogCustomInstanceBinding
|
||||
import com.github.libretube.db.DatabaseHolder.Database
|
||||
import com.github.libretube.db.obj.CustomInstance
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
class CustomInstanceDialog : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = DialogCustomInstanceBinding.inflate(layoutInflater)
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.customInstance)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.addInstance) { _, _ ->
|
||||
val instanceName = binding.instanceName.text.toString()
|
||||
val apiUrl = binding.instanceApiUrl.text.toString()
|
||||
val frontendUrl = binding.instanceFrontendUrl.text.toString()
|
||||
|
||||
if (instanceName.isNotEmpty() && apiUrl.isNotEmpty() && frontendUrl.isNotEmpty()) {
|
||||
if (apiUrl.toHttpUrlOrNull() != null && frontendUrl.toHttpUrlOrNull() != null) {
|
||||
lifecycleScope.launch {
|
||||
Database.customInstanceDao()
|
||||
.insert(CustomInstance(instanceName, apiUrl, frontendUrl))
|
||||
ActivityCompat.recreate(requireActivity())
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.invalid_url, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
// at least one empty input
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.empty_instance,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.github.libretube.ui.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.databinding.DialogCustomIntancesListBinding
|
||||
import com.github.libretube.ui.adapters.CustomInstancesAdapter
|
||||
import com.github.libretube.ui.models.CustomInstancesModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CustomInstancesListDialog: DialogFragment() {
|
||||
val viewModel: CustomInstancesModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = DialogCustomIntancesListBinding.inflate(layoutInflater)
|
||||
val adapter = CustomInstancesAdapter(
|
||||
onClickInstance = {
|
||||
CreateCustomInstanceDialog()
|
||||
.apply {
|
||||
arguments = bundleOf(IntentData.customInstance to it)
|
||||
}
|
||||
.show(childFragmentManager, null)
|
||||
},
|
||||
onDeleteInstance = {
|
||||
viewModel.deleteCustomInstance(it)
|
||||
}
|
||||
)
|
||||
binding.customInstancesRecycler.adapter = adapter
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.instances.collectLatest {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.customInstance))
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(getString(R.string.okay), null)
|
||||
.setNegativeButton(getString(R.string.addInstance), null)
|
||||
.show()
|
||||
.apply {
|
||||
getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener {
|
||||
CreateCustomInstanceDialog()
|
||||
.show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -118,12 +118,12 @@ class ShareDialog : DialogFragment() {
|
||||
)
|
||||
|
||||
// get the api urls of the other custom instances
|
||||
val customInstances = runBlocking(Dispatchers.IO) {
|
||||
Database.customInstanceDao().getAll()
|
||||
val customInstance = runBlocking(Dispatchers.IO) {
|
||||
Database.customInstanceDao().getByApiUrl(instancePref)
|
||||
}
|
||||
|
||||
// return the custom instance frontend url if available
|
||||
return customInstances.firstOrNull { it.apiUrl == instancePref }?.frontendUrl.orEmpty()
|
||||
return customInstance?.frontendUrl.orEmpty()
|
||||
}
|
||||
|
||||
private fun generateLinkText(binding: DialogShareBinding, customInstanceUrl: HttpUrl?): String {
|
||||
|
@ -0,0 +1,38 @@
|
||||
package com.github.libretube.ui.models
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.libretube.db.DatabaseHolder.Database
|
||||
import com.github.libretube.db.obj.CustomInstance
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import java.net.MalformedURLException
|
||||
|
||||
class CustomInstancesModel: ViewModel() {
|
||||
val instances = Database.customInstanceDao().getAllFlow()
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
fun addCustomInstance(apiUrlInput: String, instanceNameInput: String?, frontendUrlInput: String?) {
|
||||
if (apiUrlInput.isEmpty()) throw IllegalArgumentException()
|
||||
|
||||
val apiUrl = apiUrlInput.toHttpUrlOrNull() ?: throw MalformedURLException()
|
||||
val frontendUrl = if (!frontendUrlInput.isNullOrBlank()) {
|
||||
frontendUrlInput.toHttpUrlOrNull() ?: throw MalformedURLException()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val instanceName = instanceNameInput ?: apiUrl.host
|
||||
|
||||
Database.customInstanceDao()
|
||||
.insert(CustomInstance(instanceName, apiUrl.toString(), frontendUrl?.toString().orEmpty()))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCustomInstance(customInstance: CustomInstance) = viewModelScope.launch(Dispatchers.IO) {
|
||||
Database.customInstanceDao().deleteCustomInstance(customInstance)
|
||||
}
|
||||
}
|
@ -22,7 +22,8 @@ import com.github.libretube.extensions.toastFromMainDispatcher
|
||||
import com.github.libretube.helpers.PreferenceHelper
|
||||
import com.github.libretube.ui.adapters.InstancesAdapter
|
||||
import com.github.libretube.ui.base.BasePreferenceFragment
|
||||
import com.github.libretube.ui.dialogs.CustomInstanceDialog
|
||||
import com.github.libretube.ui.dialogs.CreateCustomInstanceDialog
|
||||
import com.github.libretube.ui.dialogs.CustomInstancesListDialog
|
||||
import com.github.libretube.ui.dialogs.DeleteAccountDialog
|
||||
import com.github.libretube.ui.dialogs.LoginDialog
|
||||
import com.github.libretube.ui.dialogs.LogoutDialog
|
||||
@ -82,17 +83,8 @@ class InstanceSettings : BasePreferenceFragment() {
|
||||
|
||||
val customInstance = findPreference<Preference>(PreferenceKeys.CUSTOM_INSTANCE)
|
||||
customInstance?.setOnPreferenceClickListener {
|
||||
CustomInstanceDialog()
|
||||
.show(childFragmentManager, CustomInstanceDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
val clearCustomInstances = findPreference<Preference>(PreferenceKeys.CLEAR_CUSTOM_INSTANCES)
|
||||
clearCustomInstances?.setOnPreferenceClickListener {
|
||||
lifecycleScope.launch {
|
||||
Database.customInstanceDao().deleteAll()
|
||||
ActivityCompat.recreate(requireActivity())
|
||||
}
|
||||
CustomInstancesListDialog()
|
||||
.show(childFragmentManager, CreateCustomInstanceDialog::class.java.name)
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
package com.github.libretube.ui.viewholders
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.databinding.CustomInstanceRowBinding
|
||||
|
||||
class CustomInstancesViewHolder(
|
||||
val binding: CustomInstanceRowBinding
|
||||
): RecyclerView.ViewHolder(binding.root)
|
31
app/src/main/res/layout/custom_instance_row.xml
Normal file
31
app/src/main/res/layout/custom_instance_row.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables"
|
||||
android:background="@drawable/rounded_ripple"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="2dp"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/instanceName"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/deleteInstance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_delete" />
|
||||
|
||||
</LinearLayout>
|
@ -5,6 +5,17 @@
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/CustomDialogTextInputLayout"
|
||||
android:hint="@string/instance_api_url">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/instanceApiUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/CustomDialogTextInputLayout"
|
||||
android:hint="@string/instance_name">
|
||||
@ -27,15 +38,4 @@
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/CustomDialogTextInputLayout"
|
||||
android:hint="@string/instance_api_url">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/instanceApiUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
10
app/src/main/res/layout/dialog_custom_intances_list.xml
Normal file
10
app/src/main/res/layout/dialog_custom_intances_list.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/custom_instances_recycler"
|
||||
android:paddingTop="10dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
@ -176,7 +176,7 @@
|
||||
<string name="resume">Resume</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="player_autoplay">Autoplay</string>
|
||||
<string name="instance_frontend_url">URL to instance frontend</string>
|
||||
<string name="instance_frontend_url">URL to instance frontend (optional)</string>
|
||||
<string name="quality">Quality</string>
|
||||
<string name="behavior">Behavior</string>
|
||||
<string name="player_summary">Defaults and behavior</string>
|
||||
|
@ -40,12 +40,6 @@
|
||||
app:summary="@string/customInstance_summary"
|
||||
app:title="@string/customInstance" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_trash"
|
||||
app:key="clearCustomInstances"
|
||||
android:dependency="full_local_mode"
|
||||
app:title="@string/clear_customInstances" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_server"
|
||||
|
Loading…
x
Reference in New Issue
Block a user