a shabby viewmodel for addserver screen

This commit is contained in:
Gary Wang 2021-12-03 00:36:48 +08:00
parent a939e6b88a
commit d19fd84244
6 changed files with 182 additions and 71 deletions

View File

@ -21,6 +21,7 @@ import net.blumia.pcmdroid.service.PlaybackService
import net.blumia.pcmdroid.ui.NavGraph import net.blumia.pcmdroid.ui.NavGraph
import net.blumia.pcmdroid.ui.screen.main.MainPlayer import net.blumia.pcmdroid.ui.screen.main.MainPlayer
import net.blumia.pcmdroid.ui.theme.PrivateCloudMusicTheme import net.blumia.pcmdroid.ui.theme.PrivateCloudMusicTheme
import net.blumia.pcmdroid.viewmodel.AddServerViewModel
import java.lang.Exception import java.lang.Exception
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -29,7 +30,10 @@ class MainActivity : ComponentActivity() {
private val browser: MediaBrowser? private val browser: MediaBrowser?
get() = if (browserFuture.isDone) browserFuture.get() else null 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) MainViewModelFactory((application as MainApplication).repository)
} }
@ -39,7 +43,7 @@ class MainActivity : ComponentActivity() {
PrivateCloudMusicTheme { PrivateCloudMusicTheme {
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) { Surface(color = MaterialTheme.colors.background) {
NavGraph(model, browser) NavGraph(mainViewModel, addServerViewModel, browser)
} }
} }
} }

View File

@ -14,6 +14,7 @@ import net.blumia.pcmdroid.model.Server
import net.blumia.pcmdroid.model.Song import net.blumia.pcmdroid.model.Song
import net.blumia.pcmdroid.model.parseFromJsonString import net.blumia.pcmdroid.model.parseFromJsonString
import net.blumia.pcmdroid.repository.ServerRepository import net.blumia.pcmdroid.repository.ServerRepository
import net.blumia.pcmdroid.viewmodel.AddServerViewModel
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -137,6 +138,9 @@ class MainViewModelFactory(private val repository: ServerRepository) : ViewModel
if (modelClass.isAssignableFrom(MainViewModel::class.java)) { if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return MainViewModel(repository) as T return MainViewModel(repository) as T
} else if (modelClass.isAssignableFrom(AddServerViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return AddServerViewModel() as T
} }
throw IllegalArgumentException("Unknown ViewModel class") throw IllegalArgumentException("Unknown ViewModel class")
} }

View File

@ -1,8 +1,11 @@
package net.blumia.pcmdroid.model package net.blumia.pcmdroid.model
import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.json.JSONException
import org.json.JSONObject
@Entity(tableName = "server_table") @Entity(tableName = "server_table")
data class Server ( data class Server (
@ -51,4 +54,25 @@ interface ServerDao {
@Query("DELETE FROM server_table") @Query("DELETE FROM server_table")
suspend fun deleteAll() suspend fun deleteAll()
}
fun parseServerFromJsonString(jsonString: String): Map<String, String> {
val kv = mutableMapOf<String, String>()
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;
} }

View File

@ -1,6 +1,7 @@
package net.blumia.pcmdroid.ui package net.blumia.pcmdroid.ui
import android.util.Log import android.util.Log
import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@ -13,7 +14,9 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import net.blumia.pcmdroid.MainApplication
import net.blumia.pcmdroid.MainViewModel import net.blumia.pcmdroid.MainViewModel
import net.blumia.pcmdroid.MainViewModelFactory
import net.blumia.pcmdroid.model.Folder import net.blumia.pcmdroid.model.Folder
import net.blumia.pcmdroid.model.Server import net.blumia.pcmdroid.model.Server
import net.blumia.pcmdroid.model.Song 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.editserver.EditServerScreen
import net.blumia.pcmdroid.ui.screen.main.MainPlayer import net.blumia.pcmdroid.ui.screen.main.MainPlayer
import net.blumia.pcmdroid.ui.screen.settings.SettingsScreen import net.blumia.pcmdroid.ui.screen.settings.SettingsScreen
import net.blumia.pcmdroid.viewmodel.AddServerViewModel
object MainDestinations { object MainDestinations {
const val MAIN_ROUTE = "main" const val MAIN_ROUTE = "main"
@ -31,7 +35,8 @@ object MainDestinations {
@Composable @Composable
fun NavGraph( fun NavGraph(
viewModel: MainViewModel, mainViewModel: MainViewModel,
addServerViewModel: AddServerViewModel,
browser: MediaBrowser?, browser: MediaBrowser?,
navController: NavHostController = rememberNavController(), navController: NavHostController = rememberNavController(),
startDestination: String = MainDestinations.MAIN_ROUTE, startDestination: String = MainDestinations.MAIN_ROUTE,
@ -42,12 +47,12 @@ fun NavGraph(
) { ) {
composable(MainDestinations.MAIN_ROUTE) { composable(MainDestinations.MAIN_ROUTE) {
val curServer: Server? by viewModel.currentServer.observeAsState(initial = null) val curServer: Server? by mainViewModel.currentServer.observeAsState(initial = null)
val servers: List<Server> by viewModel.servers.observeAsState(initial = listOf()) val servers: List<Server> by mainViewModel.servers.observeAsState(initial = listOf())
val drawerFolders: List<Folder> by viewModel.drawerFolders.observeAsState(initial = listOf()) val drawerFolders: List<Folder> by mainViewModel.drawerFolders.observeAsState(initial = listOf())
val curFolder: Folder? by viewModel.currentFolder.observeAsState(initial = null) val curFolder: Folder? by mainViewModel.currentFolder.observeAsState(initial = null)
val folders: List<Folder> by viewModel.folders.observeAsState(listOf()) val folders: List<Folder> by mainViewModel.folders.observeAsState(listOf())
val songs: List<Song> by viewModel.songs.observeAsState(listOf()) val songs: List<Song> by mainViewModel.songs.observeAsState(listOf())
MainPlayer( MainPlayer(
currentServer = curServer, currentServer = curServer,
@ -60,10 +65,10 @@ fun NavGraph(
navController.navigate(MainDestinations.ADD_SERVER_ROUTE) navController.navigate(MainDestinations.ADD_SERVER_ROUTE)
}, },
onServerItemIconClicked = { server -> onServerItemIconClicked = { server ->
viewModel.fetchServer(server) mainViewModel.fetchServer(server)
}, },
onFolderItemClicked = { folder -> onFolderItemClicked = { folder ->
viewModel.fetchFolder(folder) mainViewModel.fetchFolder(folder)
}, },
onSongItemClicked = { song, songs -> onSongItemClicked = { song, songs ->
Log.d("vvv", song.url + browser.toString()) Log.d("vvv", song.url + browser.toString())
@ -79,7 +84,7 @@ fun NavGraph(
navController.navigate( "${MainDestinations.EDIT_SERVER_ROUTE}?id=${server.serverId}") navController.navigate( "${MainDestinations.EDIT_SERVER_ROUTE}?id=${server.serverId}")
}, },
onDeleteServerActionTriggered = { server -> onDeleteServerActionTriggered = { server ->
viewModel.deleteServer(server) mainViewModel.deleteServer(server)
}, },
settingsActionTriggered = { settingsActionTriggered = {
navController.navigate(MainDestinations.SETTINGS_ROUTE) navController.navigate(MainDestinations.SETTINGS_ROUTE)
@ -93,9 +98,10 @@ fun NavGraph(
navController.navigateUp() navController.navigateUp()
}, },
onSubmitServerTriggered = { srv -> onSubmitServerTriggered = { srv ->
viewModel.addServer(srv) mainViewModel.addServer(srv)
navController.navigateUp() navController.navigateUp()
} },
viewModel = addServerViewModel,
) )
} }
@ -111,7 +117,7 @@ fun NavGraph(
"${MainDestinations.EDIT_SERVER_ROUTE}?id={id}", "${MainDestinations.EDIT_SERVER_ROUTE}?id={id}",
arguments = listOf(navArgument("id") { type = NavType.LongType }) arguments = listOf(navArgument("id") { type = NavType.LongType })
) { ) {
val srv = viewModel.getServerById(it.arguments?.getLong("id")!!) val srv = mainViewModel.getServerById(it.arguments?.getLong("id")!!)
EditServerScreen( EditServerScreen(
server = srv, server = srv,
@ -119,7 +125,7 @@ fun NavGraph(
navController.navigateUp() navController.navigateUp()
}, },
onSaveButtonTriggered = { server -> onSaveButtonTriggered = { server ->
viewModel.editServer(server) mainViewModel.editServer(server)
navController.navigateUp() navController.navigateUp()
}, },
) )

View File

@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.rounded.AddCircle import androidx.compose.material.icons.rounded.AddCircle
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.blumia.pcmdroid.model.Server import net.blumia.pcmdroid.model.Server
import net.blumia.pcmdroid.viewmodel.AddServerViewModel
@Composable @Composable
fun StepEnterApiUrl( fun StepEnterApiUrl(
urlStr: String = "", viewModel: AddServerViewModel = AddServerViewModel(),
onUrlStrChange: (String) -> Unit = {},
) { ) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -51,6 +52,9 @@ fun StepEnterApiUrl(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.subtitle2, style = MaterialTheme.typography.subtitle2,
) )
val urlStr: String by viewModel.apiUrl.observeAsState("")
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -58,8 +62,9 @@ fun StepEnterApiUrl(
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri, keyboardType = KeyboardType.Uri,
), ),
singleLine = true,
value = urlStr, value = urlStr,
onValueChange = onUrlStrChange, onValueChange = viewModel::setApiUrl,
label = { Text("Server API Url") }, label = { Text("Server API Url") },
) )
} }
@ -67,16 +72,7 @@ fun StepEnterApiUrl(
@Composable @Composable
fun StepOtherDetail( fun StepOtherDetail(
nameStr: String = "", viewModel: AddServerViewModel = AddServerViewModel(),
onNameStrChange: (String) -> Unit = {},
shortNameStr: String = "",
onShortNameStrChange: (String) -> Unit = {},
baseFolderNameStr: String = "",
onBaseFolderNameStrChange: (String) -> Unit = {},
mediaBaseUrlStr: String = "",
mediaBaseUrlStrChange: (String) -> Unit = {},
preferredFormatsStr: String = "",
preferredFormatsStrChange: (String) -> Unit = {},
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -84,12 +80,19 @@ fun StepOtherDetail(
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally, 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( OutlinedTextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 10.dp), .padding(vertical = 10.dp),
value = shortNameStr, value = shortNameStr,
onValueChange = onShortNameStrChange, onValueChange = viewModel::setShortNameStr,
label = { Text("Server short name") }, label = { Text("Server short name") },
) )
@ -98,7 +101,7 @@ fun StepOtherDetail(
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 10.dp), .padding(vertical = 10.dp),
value = nameStr, value = nameStr,
onValueChange = onNameStrChange, onValueChange = viewModel::setNameStr,
label = { Text("Server name") }, label = { Text("Server name") },
) )
@ -107,7 +110,7 @@ fun StepOtherDetail(
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 10.dp), .padding(vertical = 10.dp),
value = baseFolderNameStr, value = baseFolderNameStr,
onValueChange = onBaseFolderNameStrChange, onValueChange = viewModel::setBaseFolderNameStr,
label = { Text("Base folder name") }, label = { Text("Base folder name") },
) )
@ -116,7 +119,7 @@ fun StepOtherDetail(
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 10.dp), .padding(vertical = 10.dp),
value = mediaBaseUrlStr, value = mediaBaseUrlStr,
onValueChange = mediaBaseUrlStrChange, onValueChange = viewModel::setMediaBaseUrlStr,
label = { Text("Media base url") }, label = { Text("Media base url") },
) )
@ -125,7 +128,7 @@ fun StepOtherDetail(
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 10.dp), .padding(vertical = 10.dp),
value = preferredFormatsStr, value = preferredFormatsStr,
onValueChange = preferredFormatsStrChange, onValueChange = viewModel::setPreferredFormatsStr,
label = { Text("Preferred formats") }, label = { Text("Preferred formats") },
) )
} }
@ -136,6 +139,7 @@ fun StepOtherDetail(
fun AddServerScreen( fun AddServerScreen(
onCloseActionTriggered: () -> Unit = {}, onCloseActionTriggered: () -> Unit = {},
onSubmitServerTriggered: (Server) -> Unit = {}, onSubmitServerTriggered: (Server) -> Unit = {},
viewModel: AddServerViewModel = AddServerViewModel(),
) { ) {
var stepState by remember { mutableStateOf(0) } var stepState by remember { mutableStateOf(0) }
val finalStep = 1 val finalStep = 1
@ -172,13 +176,6 @@ fun AddServerScreen(
.padding(20.dp), .padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -188,34 +185,12 @@ fun AddServerScreen(
when (stepState) { when (stepState) {
0 -> { 0 -> {
StepEnterApiUrl( StepEnterApiUrl(
urlStr, viewModel,
onUrlStrChange = { value ->
urlStr = value
},
) )
} }
1 -> { 1 -> {
StepOtherDetail( StepOtherDetail(
nameStr, viewModel = viewModel,
onNameStrChange = { value ->
nameStr = value
},
shortNameStr,
onShortNameStrChange = { value ->
shortNameStr = value
},
baseFolderNameStr,
onBaseFolderNameStrChange = { value ->
baseFolderNameStr = value
},
mediaBaseUrlStr,
mediaBaseUrlStrChange = { value ->
mediaBaseUrlStr = value
},
preferredFormatsStr,
preferredFormatsStrChange = { value ->
preferredFormatsStr = value
},
) )
} }
} }
@ -236,15 +211,16 @@ fun AddServerScreen(
Button( Button(
onClick = { onClick = {
if (stepState != finalStep) { if (stepState != finalStep) {
viewModel.fetchContentFromApiUrl()
onNextStepButtonTriggered() onNextStepButtonTriggered()
} else { } else {
val srv = Server( val srv = Server(
url = urlStr, url = viewModel.apiUrl.value!!,
name = nameStr, name = viewModel.nameStr.value!!,
shortName = shortNameStr, shortName = viewModel.shortNameStr.value!!,
baseFolderName = baseFolderNameStr, baseFolderName = viewModel.baseFolderNameStr.value!!,
mediaBaseUrl = mediaBaseUrlStr, mediaBaseUrl = viewModel.mediaBaseUrlStr.value!!,
preferredFormats = preferredFormatsStr, preferredFormats = viewModel.preferredFormatsStr.value!!,
) )
onSubmitServerTriggered(srv) onSubmitServerTriggered(srv)
} }

View File

@ -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<String> = MutableLiveData("")
val apiUrl: LiveData<String> = _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<String> = MutableLiveData("")
val nameStr: LiveData<String> = _nameStr
fun setNameStr(name: String) {
_nameStr.postValue(name)
}
private val _shortNameStr: MutableLiveData<String> = MutableLiveData("")
val shortNameStr: LiveData<String> = _shortNameStr
fun setShortNameStr(shortName: String) {
_shortNameStr.postValue(shortName)
}
private val _baseFolderNameStr: MutableLiveData<String> = MutableLiveData("")
val baseFolderNameStr: LiveData<String> = _baseFolderNameStr
fun setBaseFolderNameStr(str: String) {
_baseFolderNameStr.postValue(str)
}
private val _mediaBaseUrlStr: MutableLiveData<String> = MutableLiveData("")
val mediaBaseUrlStr: LiveData<String> = _mediaBaseUrlStr
fun setMediaBaseUrlStr(str: String) {
_mediaBaseUrlStr.postValue(str)
}
private val _preferredFormatsStr: MutableLiveData<String> = MutableLiveData("")
val preferredFormatsStr: LiveData<String> = _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()
}
}
}
}