Multi-user support (#199)
* Add multiple users per server * Remove unnecessary longClickListener * Check if user is selected on startup * Still create JellyfinApi even if no user is selected * Already fill in the username when needing to re-login
This commit is contained in:
parent
1806b19646
commit
aeabb620ee
23 changed files with 571 additions and 68 deletions
|
@ -16,6 +16,7 @@ import com.google.android.material.navigation.NavigationBarView
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||||
import dev.jdtech.jellyfin.databinding.ActivityMainBinding
|
import dev.jdtech.jellyfin.databinding.ActivityMainBinding
|
||||||
|
import dev.jdtech.jellyfin.utils.AppPreferences
|
||||||
import dev.jdtech.jellyfin.utils.loadDownloadLocation
|
import dev.jdtech.jellyfin.utils.loadDownloadLocation
|
||||||
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -31,6 +32,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var database: ServerDatabaseDao
|
lateinit var database: ServerDatabaseDao
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appPreferences: AppPreferences
|
||||||
|
|
||||||
@OptIn(NavigationUiSaveStateControl::class)
|
@OptIn(NavigationUiSaveStateControl::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -49,6 +53,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
|
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
|
||||||
graph.setStartDestination(R.id.homeFragmentTv)
|
graph.setStartDestination(R.id.homeFragmentTv)
|
||||||
checkServersEmpty(graph)
|
checkServersEmpty(graph)
|
||||||
|
checkUser(graph)
|
||||||
if (!viewModel.startDestinationTvChanged) {
|
if (!viewModel.startDestinationTvChanged) {
|
||||||
viewModel.startDestinationTvChanged = true
|
viewModel.startDestinationTvChanged = true
|
||||||
navController.setGraph(graph, intent.extras)
|
navController.setGraph(graph, intent.extras)
|
||||||
|
@ -57,6 +62,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
checkServersEmpty(graph) {
|
checkServersEmpty(graph) {
|
||||||
navController.setGraph(graph, intent.extras)
|
navController.setGraph(graph, intent.extras)
|
||||||
}
|
}
|
||||||
|
checkUser(graph) {
|
||||||
|
navController.setGraph(graph, intent.extras)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_TELEVISION) {
|
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_TELEVISION) {
|
||||||
|
@ -108,4 +116,18 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkUser(graph: NavGraph, onNoUser: () -> Unit = {}) {
|
||||||
|
if (!viewModel.startDestinationChanged) {
|
||||||
|
appPreferences.currentServer?.let {
|
||||||
|
val currentUser = database.getServerCurrentUser(it)
|
||||||
|
if (currentUser == null) {
|
||||||
|
graph.setStartDestination(R.id.loginFragment)
|
||||||
|
viewModel.startDestinationChanged = true
|
||||||
|
onNoUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,14 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import dev.jdtech.jellyfin.databinding.UserItemBinding
|
import dev.jdtech.jellyfin.databinding.UserListItemBinding
|
||||||
import dev.jdtech.jellyfin.models.User
|
import dev.jdtech.jellyfin.models.User
|
||||||
|
|
||||||
class UserListAdapter(
|
class UserListAdapter(
|
||||||
private val clickListener: (user: User) -> Unit
|
private val clickListener: (user: User) -> Unit,
|
||||||
|
private val longClickListener: (user: User) -> Boolean
|
||||||
) : ListAdapter<User, UserListAdapter.UserViewHolder>(DiffCallback) {
|
) : ListAdapter<User, UserListAdapter.UserViewHolder>(DiffCallback) {
|
||||||
class UserViewHolder(private var binding: UserItemBinding) :
|
class UserViewHolder(private var binding: UserListItemBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(user: User) {
|
fun bind(user: User) {
|
||||||
binding.user = user
|
binding.user = user
|
||||||
|
@ -34,7 +35,7 @@ class UserListAdapter(
|
||||||
viewType: Int
|
viewType: Int
|
||||||
): UserViewHolder {
|
): UserViewHolder {
|
||||||
return UserViewHolder(
|
return UserViewHolder(
|
||||||
UserItemBinding.inflate(
|
UserListItemBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
|
@ -45,6 +46,7 @@ class UserListAdapter(
|
||||||
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
|
||||||
val user = getItem(position)
|
val user = getItem(position)
|
||||||
holder.itemView.setOnClickListener { clickListener(user) }
|
holder.itemView.setOnClickListener { clickListener(user) }
|
||||||
|
holder.itemView.setOnLongClickListener { longClickListener(user) }
|
||||||
holder.bind(user)
|
holder.bind(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
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.UserItemBinding
|
||||||
|
import dev.jdtech.jellyfin.models.User
|
||||||
|
|
||||||
|
class UserLoginListAdapter(
|
||||||
|
private val clickListener: (user: User) -> Unit
|
||||||
|
) : ListAdapter<User, UserLoginListAdapter.UserLoginViewHolder>(DiffCallback) {
|
||||||
|
class UserLoginViewHolder(private var binding: UserItemBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(user: User) {
|
||||||
|
binding.user = user
|
||||||
|
binding.executePendingBindings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object DiffCallback : DiffUtil.ItemCallback<User>() {
|
||||||
|
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): UserLoginViewHolder {
|
||||||
|
return UserLoginViewHolder(
|
||||||
|
UserItemBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: UserLoginViewHolder, position: Int) {
|
||||||
|
val user = getItem(position)
|
||||||
|
holder.itemView.setOnClickListener { clickListener(user) }
|
||||||
|
holder.bind(user)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.models.Server
|
||||||
import dev.jdtech.jellyfin.models.ServerAddress
|
import dev.jdtech.jellyfin.models.ServerAddress
|
||||||
import dev.jdtech.jellyfin.models.ServerWithAddresses
|
import dev.jdtech.jellyfin.models.ServerWithAddresses
|
||||||
import dev.jdtech.jellyfin.models.ServerWithAddressesAndUsers
|
import dev.jdtech.jellyfin.models.ServerWithAddressesAndUsers
|
||||||
|
import dev.jdtech.jellyfin.models.ServerWithUsers
|
||||||
import dev.jdtech.jellyfin.models.User
|
import dev.jdtech.jellyfin.models.User
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ interface ServerDatabaseDao {
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("select * from servers where id = :id")
|
@Query("select * from servers where id = :id")
|
||||||
fun getServerWithUsers(id: String): ServerWithAddresses
|
fun getServerWithUsers(id: String): ServerWithUsers
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("select * from servers where id = :id")
|
@Query("select * from servers where id = :id")
|
||||||
|
@ -60,4 +61,13 @@ interface ServerDatabaseDao {
|
||||||
|
|
||||||
@Query("delete from servers where id = :id")
|
@Query("delete from servers where id = :id")
|
||||||
fun delete(id: String)
|
fun delete(id: String)
|
||||||
|
|
||||||
|
@Query("delete from users where id = :id")
|
||||||
|
fun deleteUser(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 serverId = :serverId)")
|
||||||
|
fun getServerCurrentUser(serverId: String): User?
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package dev.jdtech.jellyfin.di
|
package dev.jdtech.jellyfin.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -19,7 +18,6 @@ object ApiModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun provideJellyfinApi(
|
fun provideJellyfinApi(
|
||||||
@ApplicationContext application: Context,
|
@ApplicationContext application: Context,
|
||||||
sharedPreferences: SharedPreferences,
|
|
||||||
appPreferences: AppPreferences,
|
appPreferences: AppPreferences,
|
||||||
serverDatabase: ServerDatabaseDao
|
serverDatabase: ServerDatabaseDao
|
||||||
): JellyfinApi {
|
): JellyfinApi {
|
||||||
|
@ -30,16 +28,16 @@ object ApiModule {
|
||||||
socketTimeout = appPreferences.socketTimeout
|
socketTimeout = appPreferences.socketTimeout
|
||||||
)
|
)
|
||||||
|
|
||||||
val serverId = sharedPreferences.getString("selectedServer", null)
|
val serverId = appPreferences.currentServer
|
||||||
if (serverId != null) {
|
if (serverId != null) {
|
||||||
val serverWithAddressesAndUsers = serverDatabase.getServerWithAddressesAndUsers(serverId) ?: return jellyfinApi
|
val serverWithAddressesAndUsers = serverDatabase.getServerWithAddressesAndUsers(serverId) ?: return jellyfinApi
|
||||||
val server = serverWithAddressesAndUsers.server
|
val server = serverWithAddressesAndUsers.server
|
||||||
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: return jellyfinApi
|
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: return jellyfinApi
|
||||||
val user = serverWithAddressesAndUsers.users.firstOrNull { it.id == server.currentUserId } ?: return jellyfinApi
|
val user = serverWithAddressesAndUsers.users.firstOrNull { it.id == server.currentUserId }
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.baseUrl = serverAddress.address
|
api.baseUrl = serverAddress.address
|
||||||
api.accessToken = user.accessToken
|
api.accessToken = user?.accessToken
|
||||||
userId = user.id
|
userId = user?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
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.User
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.UsersViewModel
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
|
class DeleteUserDialogFragment(private val viewModel: UsersViewModel, val user: User) : DialogFragment() {
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
return activity?.let {
|
||||||
|
val builder = MaterialAlertDialogBuilder(it)
|
||||||
|
builder.setTitle(getString(R.string.remove_user))
|
||||||
|
.setMessage(getString(R.string.remove_user_dialog_text, user.name))
|
||||||
|
.setPositiveButton(getString(R.string.remove)) { _, _ ->
|
||||||
|
viewModel.deleteUser(user)
|
||||||
|
}
|
||||||
|
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
|
||||||
|
}
|
||||||
|
builder.create()
|
||||||
|
} ?: throw IllegalStateException("Activity cannot be null")
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,12 +16,16 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.adapters.UserListAdapter
|
import dev.jdtech.jellyfin.adapters.UserLoginListAdapter
|
||||||
|
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||||
import dev.jdtech.jellyfin.databinding.FragmentLoginBinding
|
import dev.jdtech.jellyfin.databinding.FragmentLoginBinding
|
||||||
|
import dev.jdtech.jellyfin.utils.AppPreferences
|
||||||
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
|
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class LoginFragment : Fragment() {
|
class LoginFragment : Fragment() {
|
||||||
|
@ -29,6 +33,13 @@ class LoginFragment : Fragment() {
|
||||||
private lateinit var binding: FragmentLoginBinding
|
private lateinit var binding: FragmentLoginBinding
|
||||||
private lateinit var uiModeManager: UiModeManager
|
private lateinit var uiModeManager: UiModeManager
|
||||||
private val viewModel: LoginViewModel by viewModels()
|
private val viewModel: LoginViewModel by viewModels()
|
||||||
|
private val args: LoginFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appPreferences: AppPreferences
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataBase: ServerDatabaseDao
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -39,6 +50,14 @@ class LoginFragment : Fragment() {
|
||||||
uiModeManager =
|
uiModeManager =
|
||||||
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
|
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
|
||||||
|
|
||||||
|
if (args.reLogin) {
|
||||||
|
appPreferences.currentServer?.let { currentServerId ->
|
||||||
|
dataBase.getServerCurrentUser(currentServerId)?.let { user ->
|
||||||
|
(binding.editTextUsername as AppCompatEditText).setText(user.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(binding.editTextPassword as AppCompatEditText).setOnEditorActionListener { _, actionId, _ ->
|
(binding.editTextPassword as AppCompatEditText).setOnEditorActionListener { _, actionId, _ ->
|
||||||
return@setOnEditorActionListener when (actionId) {
|
return@setOnEditorActionListener when (actionId) {
|
||||||
EditorInfo.IME_ACTION_GO -> {
|
EditorInfo.IME_ACTION_GO -> {
|
||||||
|
@ -53,7 +72,7 @@ class LoginFragment : Fragment() {
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.usersRecyclerView.adapter = UserListAdapter { user ->
|
binding.usersRecyclerView.adapter = UserLoginListAdapter { user ->
|
||||||
(binding.editTextUsername as AppCompatEditText).setText(user.name)
|
(binding.editTextUsername as AppCompatEditText).setText(user.name)
|
||||||
(binding.editTextPassword as AppCompatEditText).requestFocus()
|
(binding.editTextPassword as AppCompatEditText).requestFocus()
|
||||||
}
|
}
|
||||||
|
@ -149,7 +168,7 @@ class LoginFragment : Fragment() {
|
||||||
binding.usersRecyclerView.isVisible = false
|
binding.usersRecyclerView.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
binding.usersRecyclerView.isVisible = true
|
binding.usersRecyclerView.isVisible = true
|
||||||
(binding.usersRecyclerView.adapter as UserListAdapter).submitList(users)
|
(binding.usersRecyclerView.adapter as UserLoginListAdapter).submitList(users)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,16 @@ import android.os.Bundle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.utils.AppPreferences
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
@Inject
|
||||||
|
lateinit var appPreferences: AppPreferences
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.fragment_settings, rootKey)
|
setPreferencesFromResource(R.xml.fragment_settings, rootKey)
|
||||||
|
|
||||||
|
@ -17,6 +24,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>("switchUser")?.setOnPreferenceClickListener {
|
||||||
|
val serverId = appPreferences.currentServer!!
|
||||||
|
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToUsersFragment(serverId))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
findPreference<Preference>("privacyPolicy")?.setOnPreferenceClickListener {
|
findPreference<Preference>("privacyPolicy")?.setOnPreferenceClickListener {
|
||||||
val intent = Intent(
|
val intent = Intent(
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW,
|
||||||
|
|
104
app/src/main/java/dev/jdtech/jellyfin/fragments/UsersFragment.kt
Normal file
104
app/src/main/java/dev/jdtech/jellyfin/fragments/UsersFragment.kt
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package dev.jdtech.jellyfin.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
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.AppNavigationDirections
|
||||||
|
import dev.jdtech.jellyfin.adapters.UserListAdapter
|
||||||
|
import dev.jdtech.jellyfin.databinding.FragmentUsersBinding
|
||||||
|
import dev.jdtech.jellyfin.dialogs.DeleteUserDialogFragment
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.UsersViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class UsersFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentUsersBinding
|
||||||
|
private val viewModel: UsersViewModel by viewModels()
|
||||||
|
private val args: UsersFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentUsersBinding.inflate(inflater)
|
||||||
|
|
||||||
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
binding.usersRecyclerView.adapter =
|
||||||
|
UserListAdapter(
|
||||||
|
{ user ->
|
||||||
|
viewModel.loginAsUser(user)
|
||||||
|
},
|
||||||
|
{ user ->
|
||||||
|
DeleteUserDialogFragment(viewModel, user).show(
|
||||||
|
parentFragmentManager,
|
||||||
|
"deleteUser"
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.buttonAddUser.setOnClickListener {
|
||||||
|
navigateToLoginFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 UsersViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
|
is UsersViewModel.UiState.Loading -> Unit
|
||||||
|
is UsersViewModel.UiState.Error -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.loadUsers(args.serverId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindUiStateNormal(uiState: UsersViewModel.UiState.Normal) {
|
||||||
|
(binding.usersRecyclerView.adapter as UserListAdapter).submitList(uiState.users)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToLoginFragment() {
|
||||||
|
findNavController().navigate(
|
||||||
|
AppNavigationDirections.actionGlobalLoginFragment()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToMainActivity() {
|
||||||
|
findNavController().navigate(UsersFragmentDirections.actionUsersFragmentToHomeFragment())
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,5 +10,5 @@ data class Server(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val currentServerAddressId: UUID?,
|
val currentServerAddressId: UUID?,
|
||||||
val currentUserId: UUID?,
|
var currentUserId: UUID?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,6 +12,15 @@ class AppPreferences
|
||||||
constructor(
|
constructor(
|
||||||
private val sharedPreferences: SharedPreferences
|
private val sharedPreferences: SharedPreferences
|
||||||
) {
|
) {
|
||||||
|
// Server
|
||||||
|
var currentServer: String?
|
||||||
|
get() = sharedPreferences.getString(Constants.PREF_CURRENT_SERVER, null)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putString(Constants.PREF_CURRENT_SERVER, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Appearance
|
// Appearance
|
||||||
val theme = sharedPreferences.getString(Constants.PREF_THEME, null)
|
val theme = sharedPreferences.getString(Constants.PREF_THEME, null)
|
||||||
val dynamicColors = sharedPreferences.getBoolean(Constants.PREF_DYNAMIC_COLORS, true)
|
val dynamicColors = sharedPreferences.getBoolean(Constants.PREF_DYNAMIC_COLORS, true)
|
||||||
|
|
|
@ -8,6 +8,7 @@ object Constants {
|
||||||
const val ZOOM_SCALE_THRESHOLD = 0.01f
|
const val ZOOM_SCALE_THRESHOLD = 0.01f
|
||||||
|
|
||||||
// pref
|
// pref
|
||||||
|
const val PREF_CURRENT_SERVER = "pref_current_server"
|
||||||
const val PREF_PLAYER_GESTURES = "pref_player_gestures"
|
const val PREF_PLAYER_GESTURES = "pref_player_gestures"
|
||||||
const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb"
|
const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb"
|
||||||
const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom"
|
const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom"
|
||||||
|
|
|
@ -28,7 +28,7 @@ fun Fragment.checkIfLoginRequired(error: String?) {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
if (error.contains("401")) {
|
if (error.contains("401")) {
|
||||||
Timber.d("Login required!")
|
Timber.d("Login required!")
|
||||||
findNavController().navigate(AppNavigationDirections.actionGlobalLoginFragment())
|
findNavController().navigate(AppNavigationDirections.actionGlobalLoginFragment(reLogin = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@ import dev.jdtech.jellyfin.api.JellyfinApi
|
||||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||||
import dev.jdtech.jellyfin.models.DiscoveredServer
|
import dev.jdtech.jellyfin.models.DiscoveredServer
|
||||||
import dev.jdtech.jellyfin.models.Server
|
import dev.jdtech.jellyfin.models.Server
|
||||||
|
import dev.jdtech.jellyfin.models.ServerAddress
|
||||||
|
import dev.jdtech.jellyfin.utils.AppPreferences
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -32,6 +35,7 @@ class AddServerViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: BaseApplication,
|
private val application: BaseApplication,
|
||||||
|
private val appPreferences: AppPreferences,
|
||||||
private val jellyfinApi: JellyfinApi,
|
private val jellyfinApi: JellyfinApi,
|
||||||
private val database: ServerDatabaseDao
|
private val database: ServerDatabaseDao
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
@ -152,15 +156,33 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun connectToServer(recommendedServerInfo: RecommendedServerInfo) {
|
private suspend fun connectToServer(recommendedServerInfo: RecommendedServerInfo) {
|
||||||
val serverId = recommendedServerInfo.systemInfo.getOrNull()?.id
|
val serverInfo = recommendedServerInfo.systemInfo.getOrNull()
|
||||||
?: throw Exception(resources.getString(R.string.add_server_error_no_id))
|
?: throw Exception(resources.getString(R.string.add_server_error_no_id))
|
||||||
|
|
||||||
Timber.d("Connecting to server: $serverId")
|
Timber.d("Connecting to server: ${serverInfo.serverName}")
|
||||||
|
|
||||||
if (serverAlreadyInDatabase(serverId)) {
|
if (serverAlreadyInDatabase(serverInfo.id!!)) {
|
||||||
throw Exception(resources.getString(R.string.add_server_error_already_added))
|
throw Exception(resources.getString(R.string.add_server_error_already_added))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val serverAddress = ServerAddress(
|
||||||
|
id = UUID.randomUUID(),
|
||||||
|
serverId = serverInfo.id!!,
|
||||||
|
address = recommendedServerInfo.address
|
||||||
|
)
|
||||||
|
|
||||||
|
val server = Server(
|
||||||
|
id = serverInfo.id!!,
|
||||||
|
name = serverInfo.serverName!!,
|
||||||
|
currentServerAddressId = serverAddress.id,
|
||||||
|
currentUserId = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
insertServer(server)
|
||||||
|
insertServerAddress(serverAddress)
|
||||||
|
|
||||||
|
appPreferences.currentServer = server.id
|
||||||
|
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.baseUrl = recommendedServerInfo.address
|
api.baseUrl = recommendedServerInfo.address
|
||||||
api.accessToken = null
|
api.accessToken = null
|
||||||
|
@ -221,7 +243,28 @@ constructor(
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
server = database.get(id)
|
server = database.get(id)
|
||||||
}
|
}
|
||||||
if (server != null) return true
|
return (server != null)
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add server to the database
|
||||||
|
*
|
||||||
|
* @param server The server
|
||||||
|
*/
|
||||||
|
private suspend fun insertServer(server: Server) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
database.insertServer(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add server address to the database
|
||||||
|
*
|
||||||
|
* @param address The address
|
||||||
|
*/
|
||||||
|
private suspend fun insertServerAddress(address: ServerAddress) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
database.insertServerAddress(address)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -9,10 +8,8 @@ import dev.jdtech.jellyfin.BaseApplication
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||||
import dev.jdtech.jellyfin.models.Server
|
|
||||||
import dev.jdtech.jellyfin.models.ServerAddress
|
|
||||||
import dev.jdtech.jellyfin.models.User
|
import dev.jdtech.jellyfin.models.User
|
||||||
import java.util.UUID
|
import dev.jdtech.jellyfin.utils.AppPreferences
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.Exception
|
import kotlin.Exception
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -29,7 +26,7 @@ class LoginViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
application: BaseApplication,
|
application: BaseApplication,
|
||||||
private val sharedPreferences: SharedPreferences,
|
private val appPreferences: AppPreferences,
|
||||||
private val jellyfinApi: JellyfinApi,
|
private val jellyfinApi: JellyfinApi,
|
||||||
private val database: ServerDatabaseDao
|
private val database: ServerDatabaseDao
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
@ -61,9 +58,19 @@ constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_usersState.emit(UsersState.Loading)
|
_usersState.emit(UsersState.Loading)
|
||||||
try {
|
try {
|
||||||
val publicUsers by jellyfinApi.userApi.getPublicUsers()
|
// Local users
|
||||||
val users =
|
val localUsers = appPreferences.currentServer?.let {
|
||||||
publicUsers.map { User(id = it.id, name = it.name.orEmpty(), serverId = it.serverId!!) }
|
database.getServerWithUsers(it).users
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
// Public users
|
||||||
|
val publicUsersResponse by jellyfinApi.userApi.getPublicUsers()
|
||||||
|
val publicUsers =
|
||||||
|
publicUsersResponse.map { User(id = it.id, name = it.name.orEmpty(), serverId = it.serverId!!) }
|
||||||
|
|
||||||
|
// Combine both local and public users
|
||||||
|
val users = (localUsers + publicUsers).distinctBy { it.id }
|
||||||
|
|
||||||
_usersState.emit(UsersState.Users(users))
|
_usersState.emit(UsersState.Users(users))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_usersState.emit(UsersState.Users(emptyList()))
|
_usersState.emit(UsersState.Users(emptyList()))
|
||||||
|
@ -98,26 +105,7 @@ constructor(
|
||||||
accessToken = authenticationResult.accessToken!!
|
accessToken = authenticationResult.accessToken!!
|
||||||
)
|
)
|
||||||
|
|
||||||
val serverAddress = ServerAddress(
|
insertUser(appPreferences.currentServer!!, user)
|
||||||
id = UUID.randomUUID(),
|
|
||||||
serverId = serverInfo.id!!,
|
|
||||||
address = jellyfinApi.api.baseUrl!!
|
|
||||||
)
|
|
||||||
|
|
||||||
val server = Server(
|
|
||||||
id = serverInfo.id!!,
|
|
||||||
name = serverInfo.serverName!!,
|
|
||||||
currentServerAddressId = serverAddress.id,
|
|
||||||
currentUserId = user.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
insertServer(server)
|
|
||||||
insertServerAddress(serverAddress)
|
|
||||||
insertUser(user)
|
|
||||||
|
|
||||||
val spEdit = sharedPreferences.edit()
|
|
||||||
spEdit.putString("selectedServer", server.id)
|
|
||||||
spEdit.apply()
|
|
||||||
|
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.accessToken = authenticationResult.accessToken
|
api.accessToken = authenticationResult.accessToken
|
||||||
|
@ -136,26 +124,10 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private suspend fun insertUser(serverId: String, user: User) {
|
||||||
* Add server to the database
|
|
||||||
*
|
|
||||||
* @param server The server
|
|
||||||
*/
|
|
||||||
private suspend fun insertServer(server: Server) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
database.insertServer(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun insertServerAddress(address: ServerAddress) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
database.insertServerAddress(address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun insertUser(user: User) {
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
database.insertUser(user)
|
database.insertUser(user)
|
||||||
|
database.updateServerCurrentUser(serverId, user.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
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.User
|
||||||
|
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 UsersViewModel
|
||||||
|
@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 users: List<User>) : 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 loadUsers(serverId: String) {
|
||||||
|
currentServerId = serverId
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.emit(UiState.Loading)
|
||||||
|
try {
|
||||||
|
val serverWithUser = database.getServerWithUsers(serverId)
|
||||||
|
_uiState.emit(UiState.Normal(serverWithUser.users))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.emit(UiState.Error(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete user from database
|
||||||
|
*
|
||||||
|
* @param user The user
|
||||||
|
*/
|
||||||
|
fun deleteUser(user: User) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val currentUser = database.getServerCurrentUser(currentServerId)
|
||||||
|
if (user == currentUser) {
|
||||||
|
Timber.e("You cannot delete the current user")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
database.deleteUser(user.id)
|
||||||
|
loadUsers(currentServerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loginAsUser(user: User) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val server = database.get(currentServerId) ?: return@launch
|
||||||
|
server.currentUserId = user.id
|
||||||
|
database.update(server)
|
||||||
|
|
||||||
|
jellyfinApi.apply {
|
||||||
|
api.accessToken = user.accessToken
|
||||||
|
userId = user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
_navigateToMain.emit(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
app/src/main/res/drawable/ic_plus.xml
Normal file
20
app/src/main/res/drawable/ic_plus.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12,5L12,19"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M5,12L19,12"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -2,7 +2,8 @@
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
<path
|
<path
|
||||||
android:pathData="M20,21v-2a4,4 0,0 0,-4 -4H8a4,4 0,0 0,-4 4v2"
|
android:pathData="M20,21v-2a4,4 0,0 0,-4 -4H8a4,4 0,0 0,-4 4v2"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
|
|
39
app/src/main/res/layout/fragment_users.xml
Normal file
39
app/src/main/res/layout/fragment_users.xml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout 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">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="viewModel"
|
||||||
|
type="dev.jdtech.jellyfin.viewmodels.UsersViewModel" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
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/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" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
</layout>
|
52
app/src/main/res/layout/user_list_item.xml
Normal file
52
app/src/main/res/layout/user_list_item.xml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout 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">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="user"
|
||||||
|
type="dev.jdtech.jellyfin.models.User" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="@drawable/ripple_background">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/user_image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="w,1:1"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Findroid.Image"
|
||||||
|
app:userImage="@{user}" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/user_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@{user.name}"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/user_image"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="username" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</layout>
|
|
@ -96,6 +96,9 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_navigation_settings_to_serverSelectFragment"
|
android:id="@+id/action_navigation_settings_to_serverSelectFragment"
|
||||||
app:destination="@id/serverSelectFragment" />
|
app:destination="@id/serverSelectFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_navigation_settings_to_usersFragment"
|
||||||
|
app:destination="@id/usersFragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_settingsFragment_to_about_libraries"
|
android:id="@+id/action_settingsFragment_to_about_libraries"
|
||||||
app:destination="@id/about_libraries" />
|
app:destination="@id/about_libraries" />
|
||||||
|
@ -346,6 +349,10 @@
|
||||||
app:destination="@id/homeFragmentTv"
|
app:destination="@id/homeFragmentTv"
|
||||||
app:popUpTo="@id/homeFragmentTv"
|
app:popUpTo="@id/homeFragmentTv"
|
||||||
app:popUpToInclusive="true" />
|
app:popUpToInclusive="true" />
|
||||||
|
<argument
|
||||||
|
android:name="reLogin"
|
||||||
|
app:argType="boolean"
|
||||||
|
android:defaultValue="false" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -388,4 +395,27 @@
|
||||||
android:id="@+id/action_global_loginFragment"
|
android:id="@+id/action_global_loginFragment"
|
||||||
app:destination="@id/loginFragment" />
|
app:destination="@id/loginFragment" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/usersFragment"
|
||||||
|
android:name="dev.jdtech.jellyfin.fragments.UsersFragment"
|
||||||
|
android:label="@string/users"
|
||||||
|
tools:layout="@layout/fragment_users">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_usersFragment_to_loginFragment"
|
||||||
|
app:destination="@id/loginFragment" />
|
||||||
|
<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>
|
</navigation>
|
|
@ -21,6 +21,8 @@
|
||||||
<string name="button_login">Login</string>
|
<string name="button_login">Login</string>
|
||||||
<string name="remove_server">Remove server</string>
|
<string name="remove_server">Remove server</string>
|
||||||
<string name="remove_server_dialog_text">Are you sure you want to remove the server %1$s</string>
|
<string name="remove_server_dialog_text">Are you sure you want to remove the server %1$s</string>
|
||||||
|
<string name="remove_user">Remove user</string>
|
||||||
|
<string name="remove_user_dialog_text">Are you sure you want to remove the user %1$s</string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="title_home">Home</string>
|
<string name="title_home">Home</string>
|
||||||
|
@ -142,4 +144,6 @@
|
||||||
<string name="settings_request_timeout">Request timeout (ms)</string>
|
<string name="settings_request_timeout">Request timeout (ms)</string>
|
||||||
<string name="settings_connect_timeout">Connect timeout (ms)</string>
|
<string name="settings_connect_timeout">Connect timeout (ms)</string>
|
||||||
<string name="settings_socket_timeout">Socket timeout (ms)</string>
|
<string name="settings_socket_timeout">Socket timeout (ms)</string>
|
||||||
|
<string name="users">Users</string>
|
||||||
|
<string name="add_user">Add user</string>
|
||||||
</resources>
|
</resources>
|
|
@ -11,6 +11,11 @@
|
||||||
app:key="switchServer"
|
app:key="switchServer"
|
||||||
app:title="@string/manage_servers" />
|
app:title="@string/manage_servers" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:icon="@drawable/ic_user"
|
||||||
|
app:key="switchUser"
|
||||||
|
app:title="@string/users" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
app:fragment="dev.jdtech.jellyfin.fragments.SettingsAppearanceFragment"
|
app:fragment="dev.jdtech.jellyfin.fragments.SettingsAppearanceFragment"
|
||||||
app:icon="@drawable/ic_palette"
|
app:icon="@drawable/ic_palette"
|
||||||
|
|
Loading…
Reference in a new issue