diff --git a/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt index a9d25b81..b2e58f79 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt @@ -16,6 +16,7 @@ import com.google.android.material.navigation.NavigationBarView import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.databinding.ActivityMainBinding +import dev.jdtech.jellyfin.utils.AppPreferences import dev.jdtech.jellyfin.utils.loadDownloadLocation import dev.jdtech.jellyfin.viewmodels.MainViewModel import javax.inject.Inject @@ -31,6 +32,9 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var database: ServerDatabaseDao + @Inject + lateinit var appPreferences: AppPreferences + @OptIn(NavigationUiSaveStateControl::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -49,6 +53,7 @@ class MainActivity : AppCompatActivity() { if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) { graph.setStartDestination(R.id.homeFragmentTv) checkServersEmpty(graph) + checkUser(graph) if (!viewModel.startDestinationTvChanged) { viewModel.startDestinationTvChanged = true navController.setGraph(graph, intent.extras) @@ -57,6 +62,9 @@ class MainActivity : AppCompatActivity() { checkServersEmpty(graph) { navController.setGraph(graph, intent.extras) } + checkUser(graph) { + navController.setGraph(graph, intent.extras) + } } 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() + } + } + + } + } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/adapters/UserListAdapter.kt b/app/src/main/java/dev/jdtech/jellyfin/adapters/UserListAdapter.kt index 9d3a8207..b7d35173 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/adapters/UserListAdapter.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/adapters/UserListAdapter.kt @@ -5,13 +5,14 @@ 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.databinding.UserListItemBinding import dev.jdtech.jellyfin.models.User class UserListAdapter( - private val clickListener: (user: User) -> Unit + private val clickListener: (user: User) -> Unit, + private val longClickListener: (user: User) -> Boolean ) : ListAdapter(DiffCallback) { - class UserViewHolder(private var binding: UserItemBinding) : + class UserViewHolder(private var binding: UserListItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(user: User) { binding.user = user @@ -34,7 +35,7 @@ class UserListAdapter( viewType: Int ): UserViewHolder { return UserViewHolder( - UserItemBinding.inflate( + UserListItemBinding.inflate( LayoutInflater.from(parent.context), parent, false @@ -45,6 +46,7 @@ class UserListAdapter( override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val user = getItem(position) holder.itemView.setOnClickListener { clickListener(user) } + holder.itemView.setOnLongClickListener { longClickListener(user) } holder.bind(user) } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/adapters/UserLoginListAdapter.kt b/app/src/main/java/dev/jdtech/jellyfin/adapters/UserLoginListAdapter.kt new file mode 100644 index 00000000..d28eedbb --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/adapters/UserLoginListAdapter.kt @@ -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(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() { + 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) + } +} diff --git a/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt b/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt index 5acf8620..36b62a7a 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt @@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.models.Server import dev.jdtech.jellyfin.models.ServerAddress import dev.jdtech.jellyfin.models.ServerWithAddresses import dev.jdtech.jellyfin.models.ServerWithAddressesAndUsers +import dev.jdtech.jellyfin.models.ServerWithUsers import dev.jdtech.jellyfin.models.User import java.util.UUID @@ -40,7 +41,7 @@ interface ServerDatabaseDao { @Transaction @Query("select * from servers where id = :id") - fun getServerWithUsers(id: String): ServerWithAddresses + fun getServerWithUsers(id: String): ServerWithUsers @Transaction @Query("select * from servers where id = :id") @@ -60,4 +61,13 @@ interface ServerDatabaseDao { @Query("delete from servers where id = :id") 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? } diff --git a/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt b/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt index 0933e0c1..535204fd 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt @@ -1,7 +1,6 @@ package dev.jdtech.jellyfin.di import android.content.Context -import android.content.SharedPreferences import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,7 +18,6 @@ object ApiModule { @Provides fun provideJellyfinApi( @ApplicationContext application: Context, - sharedPreferences: SharedPreferences, appPreferences: AppPreferences, serverDatabase: ServerDatabaseDao ): JellyfinApi { @@ -30,16 +28,16 @@ object ApiModule { socketTimeout = appPreferences.socketTimeout ) - val serverId = sharedPreferences.getString("selectedServer", null) + val serverId = appPreferences.currentServer if (serverId != null) { val serverWithAddressesAndUsers = serverDatabase.getServerWithAddressesAndUsers(serverId) ?: return jellyfinApi val server = serverWithAddressesAndUsers.server 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 { api.baseUrl = serverAddress.address - api.accessToken = user.accessToken - userId = user.id + api.accessToken = user?.accessToken + userId = user?.id } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/dialogs/DeleteUserDialogFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/dialogs/DeleteUserDialogFragment.kt new file mode 100644 index 00000000..906c9f4d --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/dialogs/DeleteUserDialogFragment.kt @@ -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") + } +} diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt index 709dfe98..72316b2f 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt @@ -16,12 +16,16 @@ 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.UserListAdapter +import dev.jdtech.jellyfin.adapters.UserLoginListAdapter +import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.databinding.FragmentLoginBinding +import dev.jdtech.jellyfin.utils.AppPreferences import dev.jdtech.jellyfin.viewmodels.LoginViewModel import kotlinx.coroutines.launch import timber.log.Timber +import javax.inject.Inject @AndroidEntryPoint class LoginFragment : Fragment() { @@ -29,6 +33,13 @@ class LoginFragment : Fragment() { private lateinit var binding: FragmentLoginBinding private lateinit var uiModeManager: UiModeManager 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( inflater: LayoutInflater, @@ -39,6 +50,14 @@ class LoginFragment : Fragment() { 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, _ -> return@setOnEditorActionListener when (actionId) { EditorInfo.IME_ACTION_GO -> { @@ -53,7 +72,7 @@ class LoginFragment : Fragment() { login() } - binding.usersRecyclerView.adapter = UserListAdapter { user -> + binding.usersRecyclerView.adapter = UserLoginListAdapter { user -> (binding.editTextUsername as AppCompatEditText).setText(user.name) (binding.editTextPassword as AppCompatEditText).requestFocus() } @@ -149,7 +168,7 @@ class LoginFragment : Fragment() { binding.usersRecyclerView.isVisible = false } else { binding.usersRecyclerView.isVisible = true - (binding.usersRecyclerView.adapter as UserListAdapter).submitList(users) + (binding.usersRecyclerView.adapter as UserLoginListAdapter).submitList(users) } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt index 3c088ab5..1a04d124 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt @@ -6,9 +6,16 @@ import android.os.Bundle import androidx.navigation.fragment.findNavController import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.R +import dev.jdtech.jellyfin.utils.AppPreferences +import javax.inject.Inject +@AndroidEntryPoint class SettingsFragment : PreferenceFragmentCompat() { + @Inject + lateinit var appPreferences: AppPreferences + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.fragment_settings, rootKey) @@ -17,6 +24,12 @@ class SettingsFragment : PreferenceFragmentCompat() { true } + findPreference("switchUser")?.setOnPreferenceClickListener { + val serverId = appPreferences.currentServer!! + findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToUsersFragment(serverId)) + true + } + findPreference("privacyPolicy")?.setOnPreferenceClickListener { val intent = Intent( Intent.ACTION_VIEW, diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/UsersFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/UsersFragment.kt new file mode 100644 index 00000000..5ec967ed --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/UsersFragment.kt @@ -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()) + } +} diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/Server.kt b/app/src/main/java/dev/jdtech/jellyfin/models/Server.kt index d7232bca..6e53062a 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/Server.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/Server.kt @@ -10,5 +10,5 @@ data class Server( val id: String, val name: String, val currentServerAddressId: UUID?, - val currentUserId: UUID?, + var currentUserId: UUID?, ) diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/AppPreferences.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/AppPreferences.kt index 12fb418b..fcbc578c 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/AppPreferences.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/AppPreferences.kt @@ -12,6 +12,15 @@ class AppPreferences constructor( 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 val theme = sharedPreferences.getString(Constants.PREF_THEME, null) val dynamicColors = sharedPreferences.getBoolean(Constants.PREF_DYNAMIC_COLORS, true) diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt index 408f0f7e..60e2eaa5 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt @@ -8,6 +8,7 @@ object Constants { const val ZOOM_SCALE_THRESHOLD = 0.01f // pref + const val PREF_CURRENT_SERVER = "pref_current_server" const val PREF_PLAYER_GESTURES = "pref_player_gestures" const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb" const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom" diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt index d426ff26..6176f5db 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt @@ -28,7 +28,7 @@ fun Fragment.checkIfLoginRequired(error: String?) { if (error != null) { if (error.contains("401")) { Timber.d("Login required!") - findNavController().navigate(AppNavigationDirections.actionGlobalLoginFragment()) + findNavController().navigate(AppNavigationDirections.actionGlobalLoginFragment(reLogin = true)) } } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt index 723cdd84..e0aeb963 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt @@ -11,6 +11,9 @@ import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.models.DiscoveredServer 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 kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -32,6 +35,7 @@ class AddServerViewModel @Inject constructor( private val application: BaseApplication, + private val appPreferences: AppPreferences, private val jellyfinApi: JellyfinApi, private val database: ServerDatabaseDao ) : ViewModel() { @@ -152,15 +156,33 @@ constructor( } 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)) - 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)) } + 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 { api.baseUrl = recommendedServerInfo.address api.accessToken = null @@ -221,7 +243,28 @@ constructor( withContext(Dispatchers.IO) { server = database.get(id) } - if (server != null) return true - return false + return (server != null) + } + + /** + * 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) + } } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt index fd71897c..7e2e87c5 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt @@ -1,6 +1,5 @@ package dev.jdtech.jellyfin.viewmodels -import android.content.SharedPreferences import android.content.res.Resources import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -9,10 +8,8 @@ import dev.jdtech.jellyfin.BaseApplication import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.api.JellyfinApi 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 java.util.UUID +import dev.jdtech.jellyfin.utils.AppPreferences import javax.inject.Inject import kotlin.Exception import kotlinx.coroutines.Dispatchers @@ -29,7 +26,7 @@ class LoginViewModel @Inject constructor( application: BaseApplication, - private val sharedPreferences: SharedPreferences, + private val appPreferences: AppPreferences, private val jellyfinApi: JellyfinApi, private val database: ServerDatabaseDao ) : ViewModel() { @@ -61,9 +58,19 @@ constructor( viewModelScope.launch { _usersState.emit(UsersState.Loading) try { - val publicUsers by jellyfinApi.userApi.getPublicUsers() - val users = - publicUsers.map { User(id = it.id, name = it.name.orEmpty(), serverId = it.serverId!!) } + // Local users + val localUsers = appPreferences.currentServer?.let { + 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)) } catch (e: Exception) { _usersState.emit(UsersState.Users(emptyList())) @@ -98,26 +105,7 @@ constructor( accessToken = authenticationResult.accessToken!! ) - val serverAddress = ServerAddress( - 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() + insertUser(appPreferences.currentServer!!, user) jellyfinApi.apply { api.accessToken = authenticationResult.accessToken @@ -136,26 +124,10 @@ constructor( } } - /** - * 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) { + private suspend fun insertUser(serverId: String, user: User) { withContext(Dispatchers.IO) { database.insertUser(user) + database.updateServerCurrentUser(serverId, user.id) } } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/UsersViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/UsersViewModel.kt new file mode 100644 index 00000000..da7e8104 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/UsersViewModel.kt @@ -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.Loading) + val uiState = _uiState.asStateFlow() + + sealed class UiState { + data class Normal(val users: List) : UiState() + object Loading : UiState() + data class Error(val error: Exception) : UiState() + } + + private val _navigateToMain = MutableSharedFlow() + 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) + } + } +} diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 00000000..3761fb93 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user.xml b/app/src/main/res/drawable/ic_user.xml index 8d15214a..5f6213ed 100644 --- a/app/src/main/res/drawable/ic_user.xml +++ b/app/src/main/res/drawable/ic_user.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/user_list_item.xml b/app/src/main/res/layout/user_list_item.xml new file mode 100644 index 00000000..611fdd62 --- /dev/null +++ b/app/src/main/res/layout/user_list_item.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/app_navigation.xml b/app/src/main/res/navigation/app_navigation.xml index 062bca2b..46db742e 100644 --- a/app/src/main/res/navigation/app_navigation.xml +++ b/app/src/main/res/navigation/app_navigation.xml @@ -96,6 +96,9 @@ + @@ -346,6 +349,10 @@ app:destination="@id/homeFragmentTv" app:popUpTo="@id/homeFragmentTv" app:popUpToInclusive="true" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9bb9cc7..92b3ecf4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,6 +21,8 @@ Login Remove server Are you sure you want to remove the server %1$s + Remove user + Are you sure you want to remove the user %1$s Remove Cancel Home @@ -142,4 +144,6 @@ Request timeout (ms) Connect timeout (ms) Socket timeout (ms) + Users + Add user \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml index 9fbba1c2..9d9f1998 100644 --- a/app/src/main/res/xml/fragment_settings.xml +++ b/app/src/main/res/xml/fragment_settings.xml @@ -11,6 +11,11 @@ app:key="switchServer" app:title="@string/manage_servers" /> + +