Multiple server addresses (#208)

* Add multiple addresses per server

* Clean up

* Change icon to globe

* Fix AddServerAddressDialog crashing on tv

* Fix navigation to main activity on tv

* Hide nav bar in UsersFragment and ServerAddressesFragment

* Add hint for server address
This commit is contained in:
Jarne Demeulemeester 2022-12-03 20:53:14 +01:00 committed by GitHub
parent ebea13777f
commit 6572d7e85b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 539 additions and 71 deletions

View file

@ -90,7 +90,7 @@ class MainActivity : AppCompatActivity() {
navController.addOnDestinationChangedListener { _, destination, _ ->
binding.navView!!.visibility = when (destination.id) {
R.id.twoPaneSettingsFragment, R.id.serverSelectFragment, R.id.addServerFragment, R.id.loginFragment, R.id.about_libraries_dest -> View.GONE
R.id.twoPaneSettingsFragment, R.id.serverSelectFragment, R.id.addServerFragment, R.id.loginFragment, R.id.about_libraries_dest, R.id.usersFragment, R.id.serverAddressesFragment -> View.GONE
else -> View.VISIBLE
}
if (destination.id == R.id.about_libraries_dest) binding.mainToolbar?.title =

View file

@ -0,0 +1,52 @@
package dev.jdtech.jellyfin.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.databinding.ServerAddressListItemBinding
import dev.jdtech.jellyfin.models.ServerAddress
class ServerAddressAdapter(
private val clickListener: (address: ServerAddress) -> Unit,
private val longClickListener: (address: ServerAddress) -> Boolean
) : ListAdapter<ServerAddress, ServerAddressAdapter.ServerAddressViewHolder>(DiffCallback) {
class ServerAddressViewHolder(private var binding: ServerAddressListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(address: ServerAddress) {
binding.address = address
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<ServerAddress>() {
override fun areItemsTheSame(oldItem: ServerAddress, newItem: ServerAddress): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ServerAddress, newItem: ServerAddress): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ServerAddressViewHolder {
return ServerAddressViewHolder(
ServerAddressListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ServerAddressViewHolder, position: Int) {
val address = getItem(position)
holder.itemView.setOnClickListener { clickListener(address) }
holder.itemView.setOnLongClickListener { longClickListener(address) }
holder.bind(address)
}
}

View file

@ -65,9 +65,15 @@ interface ServerDatabaseDao {
@Query("delete from users where id = :id")
fun deleteUser(id: UUID)
@Query("delete from serverAddresses where id = :id")
fun deleteServerAddress(id: UUID)
@Query("update servers set currentUserId = :userId where id = :serverId")
fun updateServerCurrentUser(serverId: String, userId: UUID)
@Query("select * from users where id = (select currentUserId from servers where id = :serverId)")
fun getServerCurrentUser(serverId: String): User?
@Query("select * from serverAddresses where id = (select currentServerAddressId from servers where id = :serverId)")
fun getServerCurrentAddress(serverId: String): ServerAddress?
}

View file

@ -0,0 +1,41 @@
package dev.jdtech.jellyfin.dialogs
import android.app.Dialog
import android.app.UiModeManager
import android.content.res.Configuration
import android.os.Bundle
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.ServerAddressesViewModel
import java.lang.IllegalStateException
class AddServerAddressDialog(
private val viewModel: ServerAddressesViewModel
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val uiModeManager =
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
val editText = EditText(this.context)
editText.hint = "http://<server_ip>:8096"
return activity?.let { activity ->
val builder = if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
AlertDialog.Builder(activity)
} else {
MaterialAlertDialogBuilder(activity)
}
builder
.setTitle("Add server address")
.setView(editText)
.setPositiveButton(getString(R.string.add)) { _, _ ->
viewModel.addAddress(editText.text.toString())
}
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}

View file

@ -0,0 +1,29 @@
package dev.jdtech.jellyfin.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.viewmodels.ServerAddressesViewModel
import java.lang.IllegalStateException
class DeleteServerAddressDialog(
private val viewModel: ServerAddressesViewModel,
val address: ServerAddress
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = MaterialAlertDialogBuilder(it)
builder.setTitle("Remove server address")
.setMessage("Are you sure you want to remove the server addres? ${address.address}")
.setPositiveButton(getString(R.string.remove)) { _, _ ->
viewModel.deleteAddress(address)
}
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}

View file

@ -0,0 +1,107 @@
package dev.jdtech.jellyfin.fragments
import android.app.UiModeManager
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.adapters.ServerAddressAdapter
import dev.jdtech.jellyfin.databinding.FragmentServerAddressesBinding
import dev.jdtech.jellyfin.dialogs.AddServerAddressDialog
import dev.jdtech.jellyfin.dialogs.DeleteServerAddressDialog
import dev.jdtech.jellyfin.viewmodels.ServerAddressesViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
@AndroidEntryPoint
class ServerAddressesFragment : Fragment() {
private lateinit var binding: FragmentServerAddressesBinding
private lateinit var uiModeManager: UiModeManager
private val viewModel: ServerAddressesViewModel by viewModels()
private val args: UsersFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentServerAddressesBinding.inflate(inflater)
uiModeManager =
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
binding.addressesRecyclerView.adapter =
ServerAddressAdapter(
{ address ->
viewModel.switchToAddress(address)
},
{ address ->
DeleteServerAddressDialog(viewModel, address).show(
parentFragmentManager,
"deleteServerAddress"
)
true
}
)
binding.buttonAddAddress.setOnClickListener {
AddServerAddressDialog(viewModel).show(
parentFragmentManager,
"addServerAddress"
)
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navigateToMain.collect {
if (it) {
navigateToMainActivity()
}
}
}
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
Timber.d("$uiState")
when (uiState) {
is ServerAddressesViewModel.UiState.Normal -> bindUiStateNormal(uiState)
is ServerAddressesViewModel.UiState.Loading -> Unit
is ServerAddressesViewModel.UiState.Error -> Unit
}
}
}
}
viewModel.loadAddresses(args.serverId)
}
fun bindUiStateNormal(uiState: ServerAddressesViewModel.UiState.Normal) {
(binding.addressesRecyclerView.adapter as ServerAddressAdapter).submitList(uiState.addresses)
}
private fun navigateToMainActivity() {
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
findNavController().navigate(UsersFragmentDirections.actionUsersFragmentToHomeFragmentTv())
} else {
findNavController().navigate(UsersFragmentDirections.actionUsersFragmentToHomeFragment())
}
}
}

View file

@ -30,6 +30,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
true
}
findPreference<Preference>("switchAddress")?.setOnPreferenceClickListener {
val serverId = appPreferences.currentServer!!
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToServerAddressesFragment(serverId))
true
}
findPreference<Preference>("privacyPolicy")?.setOnPreferenceClickListener {
val intent = Intent(
Intent.ACTION_VIEW,

View file

@ -1,9 +1,12 @@
package dev.jdtech.jellyfin.fragments
import android.app.UiModeManager
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
@ -24,6 +27,7 @@ import timber.log.Timber
class UsersFragment : Fragment() {
private lateinit var binding: FragmentUsersBinding
private lateinit var uiModeManager: UiModeManager
private val viewModel: UsersViewModel by viewModels()
private val args: UsersFragmentArgs by navArgs()
@ -33,10 +37,8 @@ class UsersFragment : Fragment() {
savedInstanceState: Bundle?
): View {
binding = FragmentUsersBinding.inflate(inflater)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
uiModeManager =
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
binding.usersRecyclerView.adapter =
UserListAdapter(
@ -99,6 +101,10 @@ class UsersFragment : Fragment() {
}
private fun navigateToMainActivity() {
findNavController().navigate(UsersFragmentDirections.actionUsersFragmentToHomeFragment())
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
findNavController().navigate(UsersFragmentDirections.actionUsersFragmentToHomeFragmentTv())
} else {
findNavController().navigate(UsersFragmentDirections.actionUsersFragmentToHomeFragment())
}
}
}

View file

@ -9,6 +9,6 @@ data class Server(
@PrimaryKey
val id: String,
val name: String,
val currentServerAddressId: UUID?,
var currentServerAddressId: UUID?,
var currentUserId: UUID?,
)

View file

@ -0,0 +1,89 @@
package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.ServerAddress
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
@HiltViewModel
class ServerAddressesViewModel
@Inject
constructor(
private val jellyfinApi: JellyfinApi,
private val database: ServerDatabaseDao,
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()
sealed class UiState {
data class Normal(val addresses: List<ServerAddress>) : UiState()
object Loading : UiState()
data class Error(val error: Exception) : UiState()
}
private val _navigateToMain = MutableSharedFlow<Boolean>()
val navigateToMain = _navigateToMain.asSharedFlow()
private var currentServerId: String = ""
fun loadAddresses(serverId: String) {
currentServerId = serverId
viewModelScope.launch {
_uiState.emit(UiState.Loading)
try {
val serverWithUser = database.getServerWithAddresses(serverId)
_uiState.emit(UiState.Normal(serverWithUser.addresses))
} catch (e: Exception) {
_uiState.emit(UiState.Error(e))
}
}
}
/**
* Delete server address from database
*
* @param address The server address
*/
fun deleteAddress(address: ServerAddress) {
viewModelScope.launch(Dispatchers.IO) {
val currentAddress = database.getServerCurrentAddress(currentServerId)
if (address == currentAddress) {
Timber.e("You cannot delete the current address")
return@launch
}
database.deleteServerAddress(address.id)
loadAddresses(currentServerId)
}
}
fun switchToAddress(address: ServerAddress) {
viewModelScope.launch {
val server = database.get(currentServerId) ?: return@launch
server.currentServerAddressId = address.id
database.update(server)
jellyfinApi.api.baseUrl = address.address
_navigateToMain.emit(true)
}
}
fun addAddress(address: String) {
viewModelScope.launch(Dispatchers.IO) {
val serverAddress = ServerAddress(UUID.randomUUID(), currentServerId, address)
database.insertServerAddress(serverAddress)
loadAddresses(currentServerId)
}
}
}

View file

@ -0,0 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M2,12L22,12"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M12,2a15.3,15.3 0,0 1,4 10,15.3 15.3,0 0,1 -4,10 15.3,15.3 0,0 1,-4 -10,15.3 15.3,0 0,1 4,-10z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/addresses_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/server_address_list_item" />
<Button
android:id="@+id/button_add_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/add_address"
app:icon="@drawable/ic_plus" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,39 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout
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">
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<data>
<variable
name="viewModel"
type="dev.jdtech.jellyfin.viewmodels.UsersViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_content"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/users_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/user_list_item" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/users_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/user_list_item" />
<Button
android:id="@+id/button_add_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/add_user"
app:icon="@drawable/ic_plus" />
<Button
android:id="@+id/button_add_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/add_user"
app:icon="@drawable/ic_plus" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/addresses_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/server_address_list_item" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/button_add_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/add_address"
app:icon="@drawable/ic_plus" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,39 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout
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">
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<data>
<variable
name="viewModel"
type="dev.jdtech.jellyfin.viewmodels.UsersViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_content"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/users_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/user_list_item" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/users_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/user_list_item" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/button_add_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/add_user"
app:icon="@drawable/ic_plus" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/button_add_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/add_user"
app:icon="@drawable/ic_plus" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="address"
type="dev.jdtech.jellyfin.models.ServerAddress" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:clickable="true"
android:focusable="true"
android:foreground="@drawable/ripple_background">
<TextView
android:id="@+id/user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{address.address}"
android:layout_margin="12dp"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
tools:text="https://..." />
</LinearLayout>
</layout>

View file

@ -99,6 +99,9 @@
<action
android:id="@+id/action_navigation_settings_to_usersFragment"
app:destination="@id/usersFragment" />
<action
android:id="@+id/action_navigation_settings_to_serverAddressesFragment"
app:destination="@id/serverAddressesFragment" />
<action
android:id="@+id/action_settingsFragment_to_about_libraries"
app:destination="@id/about_libraries" />
@ -418,4 +421,24 @@
app:argType="string" />
</fragment>
<fragment
android:id="@+id/serverAddressesFragment"
android:name="dev.jdtech.jellyfin.fragments.ServerAddressesFragment"
android:label="@string/addresses"
tools:layout="@layout/fragment_server_addresses">
<action
android:id="@+id/action_usersFragment_to_homeFragment"
app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_usersFragment_to_homeFragmentTv"
app:destination="@id/homeFragmentTv"
app:popUpTo="@id/homeFragmentTv"
app:popUpToInclusive="true" />
<argument
android:name="serverId"
app:argType="string" />
</fragment>
</navigation>

View file

@ -149,4 +149,8 @@
<string name="pref_player_mpv_vo">Video output</string>
<string name="pref_player_mpv_ao">Audio output</string>
<string name="pref_player_mpv_gpu_api" translatable="false">GPU API</string>
<string name="addresses">Addresses</string>
<string name="add_address">Add address</string>
<string name="add_server_address">Add server address</string>
<string name="add">Add</string>
</resources>

View file

@ -16,6 +16,11 @@
app:key="switchUser"
app:title="@string/users" />
<Preference
app:icon="@drawable/ic_globe"
app:key="switchAddress"
app:title="@string/addresses" />
<Preference
app:fragment="dev.jdtech.jellyfin.fragments.SettingsAppearanceFragment"
app:icon="@drawable/ic_palette"