fix livedata not updated, playlist not scrollable

This commit is contained in:
Gary Wang 2021-11-14 19:28:46 +08:00
parent 4312d1fdea
commit 8edcb91242
6 changed files with 175 additions and 34 deletions

View File

@ -52,6 +52,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0' implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.material:material-icons-extended:$compose_version" implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
@ -59,6 +60,9 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
implementation 'androidx.activity:activity-compose:1.4.0' implementation 'androidx.activity:activity-compose:1.4.0'
implementation "androidx.datastore:datastore-preferences:1.0.0" implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation("com.squareup.okhttp3:okhttp:4.9.0")
testImplementation 'junit:junit:4.+' testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View File

@ -1,63 +1,132 @@
package net.blumia.pcmdroid package net.blumia.pcmdroid
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
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
import net.blumia.pcmdroid.model.parseFromJsonString
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
class MainViewModel : ViewModel() { class MainViewModel : ViewModel() {
private val httpClient: OkHttpClient = OkHttpClient()
val servers: LiveData<List<Server>> = MutableLiveData<List<Server>>( val servers: LiveData<List<Server>> = MutableLiveData<List<Server>>(
listOf( listOf(
Server( Server(
url = Uri.parse("https://localhost/api.php"), url = "http://localhost/api.php",
name = "PCM", name = "PCM",
baseFolderName = "pcm", baseFolderName = "pcm",
), ),
Server( Server(
url = Uri.parse("https://localhost/api.php"), url = "http://localhost/api.php",
name = "PCM2", name = "PCM2",
baseFolderName = "pcm2", baseFolderName = "pcm2",
) )
) )
) )
val currentPath: LiveData<String> = MutableLiveData("") private val _currentServer: MutableLiveData<Server> = MutableLiveData(null)
val currentServer: LiveData<Server> = _currentServer;
fun setCurrentServer(srv: Server) {
_currentServer.postValue(srv)
}
val drawerFolders: LiveData<List<Folder>> = MutableLiveData( private val _currentPath: MutableLiveData<String> = MutableLiveData("")
val currentPath: LiveData<String> = _currentPath
private val _drawerFolders: MutableLiveData<List<Folder>> = MutableLiveData(
listOf( listOf(
Folder("Test/"), Folder("Test/"),
Folder("Folder/") Folder("Folder/")
) )
) )
val drawerFolders: LiveData<List<Folder>> = _drawerFolders
val folders: LiveData<List<Folder>> = MutableLiveData( private val _folders: MutableLiveData<List<Folder>> = MutableLiveData(
listOf( listOf(
Folder("Test/Converted-Modules/") Folder("Test/Converted-Modules/")
) )
) )
val folders: LiveData<List<Folder>> = _folders
val songs: LiveData<List<Song>> = MutableLiveData( private val _songs: MutableLiveData<List<Song>> = MutableLiveData(
listOf( listOf(
Song( Song(
filePath = "Test/Rick Astley - Never Gonna Give You Up.mp3", filePath = "Test/Rick Astley - Never Gonna Give You Up.mp3",
url = Uri.parse("http://localhost/rickroll.mp3") url = "http://localhost/rickroll.mp3"
), ),
Song( Song(
filePath = "Test/哇 好东西一堆.xm", filePath = "Test/哇 好东西一堆.xm",
url = Uri.parse("http://localhost/rickroll.mp3") url = "http://localhost/rickroll.mp3"
) )
) )
) )
val songs: LiveData<List<Song>> = _songs
fun fetchServer(srv: Server) { fun fetchServer(srv: Server) {
// viewModelScope.launch(context = Dispatchers.IO) {
val formBody = FormBody.Builder()
.add("do", "getfilelist")
.build()
val request = Request.Builder()
.url(srv.url)
.post(formBody)
.build()
httpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
// TODO: non-200 response will go to here too.
return@use
}
val pair = parseFromJsonString(response.body!!.string())
_drawerFolders.postValue(pair.first)
setCurrentServer(srv)
Log.d("vvv", drawerFolders.value.toString())
}
// Thread.sleep(5_000)
}
} }
fun fetchFolder(folder: Folder) { fun fetchFolder(folder: Folder) {
// if (currentServer.value == null) return
val srv = currentServer.value!!
viewModelScope.launch(context = Dispatchers.IO) {
val formBody = FormBody.Builder()
.add("do", "getfilelist")
.add("folder", folder.folderPath)
.build()
val request = Request.Builder()
.url(srv.url)
.post(formBody)
.build()
httpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
// TODO: non-200 response will go to here too.
return@use
}
val pair = parseFromJsonString(response.body!!.string())
_folders.postValue(pair.first)
_songs.postValue(pair.second)
Log.d("vvv", folders.value.toString())
}
// Thread.sleep(5_000)
}
} }
} }

View File

@ -1,9 +1,7 @@
package net.blumia.pcmdroid.model package net.blumia.pcmdroid.model
import android.net.Uri
data class Server ( data class Server (
val url: Uri, val url: String,
val name: String, val name: String,
val baseFolderName: String, val baseFolderName: String,
) )

View File

@ -1,13 +1,16 @@
package net.blumia.pcmdroid.model package net.blumia.pcmdroid.model
import android.net.Uri import android.net.Uri
import org.json.JSONException
import org.json.JSONObject
import java.net.URLDecoder
data class Song ( data class Song (
val filePath: String, // with file name val filePath: String, // with file name
val modifiedTime: Int = 0, val modifiedTime: Long = 0,
val fileSize: Int = 0, val fileSize: Int = 0,
val additionalInfo: Boolean = false, // if sidecar meta-info json file exist. val additionalInfo: Boolean = false, // if sidecar meta-info json file exist.
val url: Uri, val url: String,
) { ) {
fun displayName(): String { fun displayName(): String {
return Uri.parse(filePath).lastPathSegment ?: "???" return Uri.parse(filePath).lastPathSegment ?: "???"
@ -18,6 +21,41 @@ data class Folder (
val folderPath: String, val folderPath: String,
) { ) {
fun displayName(): String { fun displayName(): String {
return Uri.parse(folderPath).lastPathSegment ?: "???" return URLDecoder.decode(Uri.parse(folderPath).lastPathSegment ?: "???", "UTF-8")
} }
}
fun parseFromJsonString(jsonString: String): Pair<List<Folder>, List<Song>> {
val folders = mutableListOf<Folder>()
val songs = mutableListOf<Song>()
try {
val jsonObj = JSONObject(jsonString)
val result = jsonObj.getJSONObject("result")
val data = result.getJSONObject("data")
val subFolders = data.getJSONArray("subFolderList")
val musicList = data.getJSONArray("musicList")
for (i in 0 until subFolders.length()) {
val rawFolderName = subFolders.getString(i)
folders.add(Folder(rawFolderName))
}
for (i in 0 until musicList.length()) {
val fileObject = musicList.getJSONObject(i)
val rawFileName = fileObject.getString("fileName")
val fileName = URLDecoder.decode(rawFileName, "UTF-8")
val modifiedTime = fileObject.getLong("modifiedTime")
val fileSize = fileObject.getLong("fileSize")
songs.add(
Song(
rawFileName,
modifiedTime = modifiedTime,
url = rawFileName
)
)
}
} catch (e: JSONException) {
e.printStackTrace()
}
return Pair(folders, songs)
} }

View File

@ -1,11 +1,16 @@
package net.blumia.pcmdroid.ui package net.blumia.pcmdroid.ui
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost 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 net.blumia.pcmdroid.MainViewModel import net.blumia.pcmdroid.MainViewModel
import net.blumia.pcmdroid.model.Folder
import net.blumia.pcmdroid.model.Server
import net.blumia.pcmdroid.model.Song
import net.blumia.pcmdroid.ui.screen.addserver.AddServerScreen import net.blumia.pcmdroid.ui.screen.addserver.AddServerScreen
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
@ -27,11 +32,26 @@ fun NavGraph(
startDestination = startDestination startDestination = startDestination
) { ) {
composable(MainDestinations.MAIN_ROUTE) { composable(MainDestinations.MAIN_ROUTE) {
val servers: List<Server> by viewModel.servers.observeAsState(initial = listOf())
val drawerFolders: List<Folder> by viewModel.drawerFolders.observeAsState(initial = listOf())
val folders: List<Folder> by viewModel.folders.observeAsState(listOf())
val songs: List<Song> by viewModel.songs.observeAsState(listOf())
MainPlayer( MainPlayer(
viewModel = viewModel, servers = servers,
drawerFolders = drawerFolders,
folders = folders,
songs = songs,
addServerActionTriggered = { addServerActionTriggered = {
navController.navigate(MainDestinations.ADD_SERVER_ROUTE) navController.navigate(MainDestinations.ADD_SERVER_ROUTE)
}, },
onServerItemIconClicked = { server ->
viewModel.fetchServer(server)
},
onDrawerFolderItemClicked = { folder ->
viewModel.fetchFolder(folder)
},
settingsActionTriggered = { settingsActionTriggered = {
navController.navigate(MainDestinations.SETTINGS_ROUTE) navController.navigate(MainDestinations.SETTINGS_ROUTE)
} }

View File

@ -1,12 +1,9 @@
package net.blumia.pcmdroid.ui.screen.main package net.blumia.pcmdroid.ui.screen.main
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.Image import androidx.compose.foundation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@ -15,6 +12,9 @@ import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.PlayCircle import androidx.compose.material.icons.rounded.PlayCircle
import androidx.compose.material.icons.rounded.PlayCircleOutline import androidx.compose.material.icons.rounded.PlayCircleOutline
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -24,7 +24,6 @@ import androidx.compose.ui.res.stringResource
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 kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.blumia.pcmdroid.MainViewModel
import net.blumia.pcmdroid.R import net.blumia.pcmdroid.R
import net.blumia.pcmdroid.model.Folder import net.blumia.pcmdroid.model.Folder
import net.blumia.pcmdroid.model.Server import net.blumia.pcmdroid.model.Server
@ -74,15 +73,17 @@ fun NowPlaying() {
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun DrawerContent( fun DrawerContent(
viewModel: MainViewModel = MainViewModel(), servers: List<Server> = listOf(),
drawerFolders: List<Folder> = listOf(),
addServerActionTriggered: () -> Unit = {}, addServerActionTriggered: () -> Unit = {},
onServerItemIconClicked: (Server) -> Unit = {},
onDrawerFolderItemClicked: (Folder) -> Unit = {},
settingsActionTriggered: () -> Unit = {}, settingsActionTriggered: () -> Unit = {},
) { ) {
Row(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxSize()) {
var selectedItem by remember { mutableStateOf<Int?>(0) } var selectedItem by remember { mutableStateOf<Int?>(0) }
val servers = viewModel.servers.value
NavigationRail { NavigationRail {
servers?.forEachIndexed { index, server -> servers.forEachIndexed { index, server ->
NavigationRailItem( NavigationRailItem(
icon = { icon = {
Icon( Icon(
@ -94,7 +95,7 @@ fun DrawerContent(
}, },
selected = selectedItem == index, selected = selectedItem == index,
onClick = { onClick = {
viewModel.fetchServer(server) onServerItemIconClicked(server)
} }
) )
} }
@ -133,11 +134,10 @@ fun DrawerContent(
Divider() Divider()
val drawerFolders = viewModel.drawerFolders.value drawerFolders.forEach { folder ->
drawerFolders?.forEach { folder ->
ListItem( ListItem(
modifier = Modifier.clickable{ modifier = Modifier.clickable{
viewModel.fetchFolder(folder) onDrawerFolderItemClicked(folder)
}, },
text = { text = {
Text( Text(
@ -155,8 +155,13 @@ fun DrawerContent(
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun MainPlayer( fun MainPlayer(
viewModel: MainViewModel = MainViewModel(), servers: List<Server> = listOf(),
drawerFolders: List<Folder> = listOf(),
folders: List<Folder> = listOf(),
songs: List<Song> = listOf(),
addServerActionTriggered: () -> Unit = {}, addServerActionTriggered: () -> Unit = {},
onServerItemIconClicked: (Server) -> Unit = {},
onDrawerFolderItemClicked: (Folder) -> Unit = {},
settingsActionTriggered: () -> Unit = {}, settingsActionTriggered: () -> Unit = {},
) { ) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
@ -165,8 +170,11 @@ fun MainPlayer(
drawerState = drawerState, drawerState = drawerState,
drawerContent = { drawerContent = {
DrawerContent( DrawerContent(
viewModel = viewModel, servers = servers,
drawerFolders = drawerFolders,
addServerActionTriggered = addServerActionTriggered, addServerActionTriggered = addServerActionTriggered,
onServerItemIconClicked = onServerItemIconClicked,
onDrawerFolderItemClicked = onDrawerFolderItemClicked,
settingsActionTriggered = settingsActionTriggered, settingsActionTriggered = settingsActionTriggered,
) )
} }
@ -195,7 +203,11 @@ fun MainPlayer(
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Column(modifier = Modifier.weight(1f, fill = true)) { Column(
modifier = Modifier
.weight(1f, fill = true)
.verticalScroll(rememberScrollState())
) {
val iconLeftPadding = 16.dp val iconLeftPadding = 16.dp
val iconVerticalPadding = 12.dp val iconVerticalPadding = 12.dp
@ -229,7 +241,7 @@ fun MainPlayer(
} }
} }
viewModel.folders.value?.forEach { folder -> folders.forEach { folder ->
ListItem( ListItem(
modifier = Modifier.clickable { }, modifier = Modifier.clickable { },
icon = { icon = {
@ -244,7 +256,7 @@ fun MainPlayer(
) )
} }
viewModel.songs.value?.forEach { song -> songs.forEach { song ->
ListItem( ListItem(
modifier = Modifier.clickable { }, modifier = Modifier.clickable { },
icon = { icon = {