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.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)
}
}
}

View File

@ -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")
}

View File

@ -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<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
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<Server> by viewModel.servers.observeAsState(initial = listOf())
val drawerFolders: List<Folder> by viewModel.drawerFolders.observeAsState(initial = listOf())
val curFolder: Folder? by viewModel.currentFolder.observeAsState(initial = null)
val folders: List<Folder> by viewModel.folders.observeAsState(listOf())
val songs: List<Song> by viewModel.songs.observeAsState(listOf())
val curServer: Server? by mainViewModel.currentServer.observeAsState(initial = null)
val servers: List<Server> by mainViewModel.servers.observeAsState(initial = listOf())
val drawerFolders: List<Folder> by mainViewModel.drawerFolders.observeAsState(initial = listOf())
val curFolder: Folder? by mainViewModel.currentFolder.observeAsState(initial = null)
val folders: List<Folder> by mainViewModel.folders.observeAsState(listOf())
val songs: List<Song> 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()
},
)

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.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)
}

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()
}
}
}
}