diff --git a/app/src/main/java/net/blumia/pcmdroid/MainActivity.kt b/app/src/main/java/net/blumia/pcmdroid/MainActivity.kt index 2d913cd..6ab4e8b 100644 --- a/app/src/main/java/net/blumia/pcmdroid/MainActivity.kt +++ b/app/src/main/java/net/blumia/pcmdroid/MainActivity.kt @@ -21,6 +21,7 @@ import net.blumia.pcmdroid.service.PlaybackService import net.blumia.pcmdroid.ui.NavGraph import net.blumia.pcmdroid.ui.screen.main.MainPlayer import net.blumia.pcmdroid.ui.theme.PrivateCloudMusicTheme +import net.blumia.pcmdroid.viewmodel.AddServerViewModel import java.lang.Exception class MainActivity : ComponentActivity() { @@ -29,7 +30,10 @@ class MainActivity : ComponentActivity() { private val browser: MediaBrowser? get() = if (browserFuture.isDone) browserFuture.get() else null - private val model: MainViewModel by viewModels { + private val mainViewModel: MainViewModel by viewModels { + MainViewModelFactory((application as MainApplication).repository) + } + private val addServerViewModel: AddServerViewModel by viewModels { MainViewModelFactory((application as MainApplication).repository) } @@ -39,7 +43,7 @@ class MainActivity : ComponentActivity() { PrivateCloudMusicTheme { // A surface container using the 'background' color from the theme Surface(color = MaterialTheme.colors.background) { - NavGraph(model, browser) + NavGraph(mainViewModel, addServerViewModel, browser) } } } diff --git a/app/src/main/java/net/blumia/pcmdroid/MainViewModel.kt b/app/src/main/java/net/blumia/pcmdroid/MainViewModel.kt index dbcec23..94e9c5e 100644 --- a/app/src/main/java/net/blumia/pcmdroid/MainViewModel.kt +++ b/app/src/main/java/net/blumia/pcmdroid/MainViewModel.kt @@ -14,6 +14,7 @@ import net.blumia.pcmdroid.model.Server import net.blumia.pcmdroid.model.Song import net.blumia.pcmdroid.model.parseFromJsonString import net.blumia.pcmdroid.repository.ServerRepository +import net.blumia.pcmdroid.viewmodel.AddServerViewModel import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request @@ -137,6 +138,9 @@ class MainViewModelFactory(private val repository: ServerRepository) : ViewModel if (modelClass.isAssignableFrom(MainViewModel::class.java)) { @Suppress("UNCHECKED_CAST") return MainViewModel(repository) as T + } else if (modelClass.isAssignableFrom(AddServerViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return AddServerViewModel() as T } throw IllegalArgumentException("Unknown ViewModel class") } diff --git a/app/src/main/java/net/blumia/pcmdroid/model/Server.kt b/app/src/main/java/net/blumia/pcmdroid/model/Server.kt index 748e4a0..035ebf5 100644 --- a/app/src/main/java/net/blumia/pcmdroid/model/Server.kt +++ b/app/src/main/java/net/blumia/pcmdroid/model/Server.kt @@ -1,8 +1,11 @@ package net.blumia.pcmdroid.model +import android.util.Log import androidx.lifecycle.LiveData import androidx.room.* import kotlinx.coroutines.flow.Flow +import org.json.JSONException +import org.json.JSONObject @Entity(tableName = "server_table") data class Server ( @@ -51,4 +54,25 @@ interface ServerDao { @Query("DELETE FROM server_table") suspend fun deleteAll() +} + +fun parseServerFromJsonString(jsonString: String): Map { + + val kv = mutableMapOf() + + try { + val jsonObj = JSONObject(jsonString) + val result = jsonObj.getJSONObject("result") + + kv["serverName"] = result.getString("serverName") + kv["serverShortName"] = result.getString("serverShortName") + kv["baseFolderNameHint"] = result.getString("baseFolderNameHint") + kv["preferredFormatsHint"] = result.getString("preferredFormatsHint") + kv["mediaRootUrl"] = result.getString("mediaRootUrl") + } catch (e: JSONException) { + e.printStackTrace() + Log.d("vvv", jsonString) + } + + return kv; } \ No newline at end of file diff --git a/app/src/main/java/net/blumia/pcmdroid/ui/NavGraph.kt b/app/src/main/java/net/blumia/pcmdroid/ui/NavGraph.kt index a4034d8..da5b17c 100644 --- a/app/src/main/java/net/blumia/pcmdroid/ui/NavGraph.kt +++ b/app/src/main/java/net/blumia/pcmdroid/ui/NavGraph.kt @@ -1,6 +1,7 @@ package net.blumia.pcmdroid.ui import android.util.Log +import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -13,7 +14,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import net.blumia.pcmdroid.MainApplication import net.blumia.pcmdroid.MainViewModel +import net.blumia.pcmdroid.MainViewModelFactory import net.blumia.pcmdroid.model.Folder import net.blumia.pcmdroid.model.Server import net.blumia.pcmdroid.model.Song @@ -21,6 +24,7 @@ import net.blumia.pcmdroid.ui.screen.addserver.AddServerScreen import net.blumia.pcmdroid.ui.screen.editserver.EditServerScreen import net.blumia.pcmdroid.ui.screen.main.MainPlayer import net.blumia.pcmdroid.ui.screen.settings.SettingsScreen +import net.blumia.pcmdroid.viewmodel.AddServerViewModel object MainDestinations { const val MAIN_ROUTE = "main" @@ -31,7 +35,8 @@ object MainDestinations { @Composable fun NavGraph( - viewModel: MainViewModel, + mainViewModel: MainViewModel, + addServerViewModel: AddServerViewModel, browser: MediaBrowser?, navController: NavHostController = rememberNavController(), startDestination: String = MainDestinations.MAIN_ROUTE, @@ -42,12 +47,12 @@ fun NavGraph( ) { composable(MainDestinations.MAIN_ROUTE) { - val curServer: Server? by viewModel.currentServer.observeAsState(initial = null) - val servers: List by viewModel.servers.observeAsState(initial = listOf()) - val drawerFolders: List by viewModel.drawerFolders.observeAsState(initial = listOf()) - val curFolder: Folder? by viewModel.currentFolder.observeAsState(initial = null) - val folders: List by viewModel.folders.observeAsState(listOf()) - val songs: List by viewModel.songs.observeAsState(listOf()) + val curServer: Server? by mainViewModel.currentServer.observeAsState(initial = null) + val servers: List by mainViewModel.servers.observeAsState(initial = listOf()) + val drawerFolders: List by mainViewModel.drawerFolders.observeAsState(initial = listOf()) + val curFolder: Folder? by mainViewModel.currentFolder.observeAsState(initial = null) + val folders: List by mainViewModel.folders.observeAsState(listOf()) + val songs: List by mainViewModel.songs.observeAsState(listOf()) MainPlayer( currentServer = curServer, @@ -60,10 +65,10 @@ fun NavGraph( navController.navigate(MainDestinations.ADD_SERVER_ROUTE) }, onServerItemIconClicked = { server -> - viewModel.fetchServer(server) + mainViewModel.fetchServer(server) }, onFolderItemClicked = { folder -> - viewModel.fetchFolder(folder) + mainViewModel.fetchFolder(folder) }, onSongItemClicked = { song, songs -> Log.d("vvv", song.url + browser.toString()) @@ -79,7 +84,7 @@ fun NavGraph( navController.navigate( "${MainDestinations.EDIT_SERVER_ROUTE}?id=${server.serverId}") }, onDeleteServerActionTriggered = { server -> - viewModel.deleteServer(server) + mainViewModel.deleteServer(server) }, settingsActionTriggered = { navController.navigate(MainDestinations.SETTINGS_ROUTE) @@ -93,9 +98,10 @@ fun NavGraph( navController.navigateUp() }, onSubmitServerTriggered = { srv -> - viewModel.addServer(srv) + mainViewModel.addServer(srv) navController.navigateUp() - } + }, + viewModel = addServerViewModel, ) } @@ -111,7 +117,7 @@ fun NavGraph( "${MainDestinations.EDIT_SERVER_ROUTE}?id={id}", arguments = listOf(navArgument("id") { type = NavType.LongType }) ) { - val srv = viewModel.getServerById(it.arguments?.getLong("id")!!) + val srv = mainViewModel.getServerById(it.arguments?.getLong("id")!!) EditServerScreen( server = srv, @@ -119,7 +125,7 @@ fun NavGraph( navController.navigateUp() }, onSaveButtonTriggered = { server -> - viewModel.editServer(server) + mainViewModel.editServer(server) navController.navigateUp() }, ) diff --git a/app/src/main/java/net/blumia/pcmdroid/ui/screen/addserver/AddServer.kt b/app/src/main/java/net/blumia/pcmdroid/ui/screen/addserver/AddServer.kt index 4d433cd..3dc23b4 100644 --- a/app/src/main/java/net/blumia/pcmdroid/ui/screen/addserver/AddServer.kt +++ b/app/src/main/java/net/blumia/pcmdroid/ui/screen/addserver/AddServer.kt @@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.rounded.AddCircle import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -20,11 +21,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.blumia.pcmdroid.model.Server +import net.blumia.pcmdroid.viewmodel.AddServerViewModel @Composable fun StepEnterApiUrl( - urlStr: String = "", - onUrlStrChange: (String) -> Unit = {}, + viewModel: AddServerViewModel = AddServerViewModel(), ) { Column( modifier = Modifier.fillMaxSize(), @@ -51,6 +52,9 @@ fun StepEnterApiUrl( textAlign = TextAlign.Center, style = MaterialTheme.typography.subtitle2, ) + + val urlStr: String by viewModel.apiUrl.observeAsState("") + OutlinedTextField( modifier = Modifier .fillMaxWidth() @@ -58,8 +62,9 @@ fun StepEnterApiUrl( keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Uri, ), + singleLine = true, value = urlStr, - onValueChange = onUrlStrChange, + onValueChange = viewModel::setApiUrl, label = { Text("Server API Url") }, ) } @@ -67,16 +72,7 @@ fun StepEnterApiUrl( @Composable fun StepOtherDetail( - nameStr: String = "", - onNameStrChange: (String) -> Unit = {}, - shortNameStr: String = "", - onShortNameStrChange: (String) -> Unit = {}, - baseFolderNameStr: String = "", - onBaseFolderNameStrChange: (String) -> Unit = {}, - mediaBaseUrlStr: String = "", - mediaBaseUrlStrChange: (String) -> Unit = {}, - preferredFormatsStr: String = "", - preferredFormatsStrChange: (String) -> Unit = {}, + viewModel: AddServerViewModel = AddServerViewModel(), ) { Column( modifier = Modifier @@ -84,12 +80,19 @@ fun StepOtherDetail( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { + + val nameStr: String by viewModel.nameStr.observeAsState("") + val shortNameStr: String by viewModel.shortNameStr.observeAsState("") + val baseFolderNameStr: String by viewModel.baseFolderNameStr.observeAsState("") + val mediaBaseUrlStr: String by viewModel.mediaBaseUrlStr.observeAsState("") + val preferredFormatsStr: String by viewModel.preferredFormatsStr.observeAsState("") + OutlinedTextField( modifier = Modifier .fillMaxWidth() .padding(vertical = 10.dp), value = shortNameStr, - onValueChange = onShortNameStrChange, + onValueChange = viewModel::setShortNameStr, label = { Text("Server short name") }, ) @@ -98,7 +101,7 @@ fun StepOtherDetail( .fillMaxWidth() .padding(vertical = 10.dp), value = nameStr, - onValueChange = onNameStrChange, + onValueChange = viewModel::setNameStr, label = { Text("Server name") }, ) @@ -107,7 +110,7 @@ fun StepOtherDetail( .fillMaxWidth() .padding(vertical = 10.dp), value = baseFolderNameStr, - onValueChange = onBaseFolderNameStrChange, + onValueChange = viewModel::setBaseFolderNameStr, label = { Text("Base folder name") }, ) @@ -116,7 +119,7 @@ fun StepOtherDetail( .fillMaxWidth() .padding(vertical = 10.dp), value = mediaBaseUrlStr, - onValueChange = mediaBaseUrlStrChange, + onValueChange = viewModel::setMediaBaseUrlStr, label = { Text("Media base url") }, ) @@ -125,7 +128,7 @@ fun StepOtherDetail( .fillMaxWidth() .padding(vertical = 10.dp), value = preferredFormatsStr, - onValueChange = preferredFormatsStrChange, + onValueChange = viewModel::setPreferredFormatsStr, label = { Text("Preferred formats") }, ) } @@ -136,6 +139,7 @@ fun StepOtherDetail( fun AddServerScreen( onCloseActionTriggered: () -> Unit = {}, onSubmitServerTriggered: (Server) -> Unit = {}, + viewModel: AddServerViewModel = AddServerViewModel(), ) { var stepState by remember { mutableStateOf(0) } val finalStep = 1 @@ -172,13 +176,6 @@ fun AddServerScreen( .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - var urlStr by rememberSaveable { mutableStateOf("") } - var nameStr by rememberSaveable { mutableStateOf("") } - var shortNameStr by rememberSaveable { mutableStateOf("") } - var baseFolderNameStr by rememberSaveable { mutableStateOf("") } - var mediaBaseUrlStr by rememberSaveable { mutableStateOf("") } - var preferredFormatsStr by rememberSaveable { mutableStateOf("") } - Column( modifier = Modifier .fillMaxWidth() @@ -188,34 +185,12 @@ fun AddServerScreen( when (stepState) { 0 -> { StepEnterApiUrl( - urlStr, - onUrlStrChange = { value -> - urlStr = value - }, + viewModel, ) } 1 -> { StepOtherDetail( - nameStr, - onNameStrChange = { value -> - nameStr = value - }, - shortNameStr, - onShortNameStrChange = { value -> - shortNameStr = value - }, - baseFolderNameStr, - onBaseFolderNameStrChange = { value -> - baseFolderNameStr = value - }, - mediaBaseUrlStr, - mediaBaseUrlStrChange = { value -> - mediaBaseUrlStr = value - }, - preferredFormatsStr, - preferredFormatsStrChange = { value -> - preferredFormatsStr = value - }, + viewModel = viewModel, ) } } @@ -236,15 +211,16 @@ fun AddServerScreen( Button( onClick = { if (stepState != finalStep) { + viewModel.fetchContentFromApiUrl() onNextStepButtonTriggered() } else { val srv = Server( - url = urlStr, - name = nameStr, - shortName = shortNameStr, - baseFolderName = baseFolderNameStr, - mediaBaseUrl = mediaBaseUrlStr, - preferredFormats = preferredFormatsStr, + url = viewModel.apiUrl.value!!, + name = viewModel.nameStr.value!!, + shortName = viewModel.shortNameStr.value!!, + baseFolderName = viewModel.baseFolderNameStr.value!!, + mediaBaseUrl = viewModel.mediaBaseUrlStr.value!!, + preferredFormats = viewModel.preferredFormatsStr.value!!, ) onSubmitServerTriggered(srv) } diff --git a/app/src/main/java/net/blumia/pcmdroid/viewmodel/AddServerViewModel.kt b/app/src/main/java/net/blumia/pcmdroid/viewmodel/AddServerViewModel.kt new file mode 100644 index 0000000..62fcb69 --- /dev/null +++ b/app/src/main/java/net/blumia/pcmdroid/viewmodel/AddServerViewModel.kt @@ -0,0 +1,97 @@ +package net.blumia.pcmdroid.viewmodel + +import android.util.Log +import android.webkit.URLUtil +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.blumia.pcmdroid.model.Server +import net.blumia.pcmdroid.model.parseFromJsonString +import net.blumia.pcmdroid.model.parseServerFromJsonString +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request + +class AddServerViewModel : ViewModel() { +// private val httpClient: OkHttpClient = OkHttpClient() + + private val _apiUrl: MutableLiveData = MutableLiveData("") + val apiUrl: LiveData = _apiUrl + fun setApiUrl(url: String) { + // FIXME: do we really need to use postValue here? + // will the setter for the LiveData object trigger observer? + _apiUrl.postValue(url) + } + + private val _nameStr: MutableLiveData = MutableLiveData("") + val nameStr: LiveData = _nameStr + fun setNameStr(name: String) { + _nameStr.postValue(name) + } + + private val _shortNameStr: MutableLiveData = MutableLiveData("") + val shortNameStr: LiveData = _shortNameStr + fun setShortNameStr(shortName: String) { + _shortNameStr.postValue(shortName) + } + + private val _baseFolderNameStr: MutableLiveData = MutableLiveData("") + val baseFolderNameStr: LiveData = _baseFolderNameStr + fun setBaseFolderNameStr(str: String) { + _baseFolderNameStr.postValue(str) + } + + private val _mediaBaseUrlStr: MutableLiveData = MutableLiveData("") + val mediaBaseUrlStr: LiveData = _mediaBaseUrlStr + fun setMediaBaseUrlStr(str: String) { + _mediaBaseUrlStr.postValue(str) + } + + private val _preferredFormatsStr: MutableLiveData = MutableLiveData("") + val preferredFormatsStr: LiveData = _preferredFormatsStr + fun setPreferredFormatsStr(str: String) { + _preferredFormatsStr.postValue(str) + } + + fun isApiUrlValid(): Boolean { + return URLUtil.isNetworkUrl(apiUrl.value) + } + + fun fetchContentFromApiUrl() { + val httpClient: OkHttpClient = OkHttpClient() + + viewModelScope.launch(context = Dispatchers.IO) { + + val formBody = FormBody.Builder() + .add("do", "getserverinfo") + .build() + + val request = Request.Builder() +// .url("http://localhost/") + .url(apiUrl.value!!) + .post(formBody) + .build() + + try { + httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + // TODO: non-200 response will go to here too. + return@use + } + val kv = parseServerFromJsonString(response.body!!.string()) + Log.d("vvv", kv.toString()) + if (kv["serverName"] != null) setNameStr(kv["serverName"]!!) + if (kv["serverShortName"] != null) setShortNameStr(kv["serverShortName"]!!) + if (kv["baseFolderNameHint"] != null) setBaseFolderNameStr(kv["baseFolderNameHint"]!!) + if (kv["preferredFormatsHint"] != null) setPreferredFormatsStr(kv["preferredFormatsHint"]!!) + if (kv["mediaRootUrl"] != null) setMediaBaseUrlStr(kv["mediaRootUrl"]!!) + } + } catch (e: java.io.IOException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file