From 8edcb9124223a4c25360207a455fe8f19fe27102 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Sun, 14 Nov 2021 19:28:46 +0800 Subject: [PATCH] fix livedata not updated, playlist not scrollable --- app/build.gradle | 4 + .../java/net/blumia/pcmdroid/MainViewModel.kt | 89 ++++++++++++++++--- .../java/net/blumia/pcmdroid/model/Server.kt | 4 +- .../java/net/blumia/pcmdroid/model/Song.kt | 44 ++++++++- .../java/net/blumia/pcmdroid/ui/NavGraph.kt | 22 ++++- .../pcmdroid/ui/screen/main/MainPlayer.kt | 46 ++++++---- 6 files changed, 175 insertions(+), 34 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f03097f..0fed5ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,6 +52,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'com.google.android.material:material:1.4.0' 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-icons-extended:$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.activity:activity-compose:1.4.0' implementation "androidx.datastore:datastore-preferences:1.0.0" + + implementation("com.squareup.okhttp3:okhttp:4.9.0") + testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/java/net/blumia/pcmdroid/MainViewModel.kt b/app/src/main/java/net/blumia/pcmdroid/MainViewModel.kt index 64cec9c..d37387e 100644 --- a/app/src/main/java/net/blumia/pcmdroid/MainViewModel.kt +++ b/app/src/main/java/net/blumia/pcmdroid/MainViewModel.kt @@ -1,63 +1,132 @@ package net.blumia.pcmdroid import android.net.Uri +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData 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.Server import net.blumia.pcmdroid.model.Song +import net.blumia.pcmdroid.model.parseFromJsonString +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request class MainViewModel : ViewModel() { + private val httpClient: OkHttpClient = OkHttpClient() + val servers: LiveData> = MutableLiveData>( listOf( Server( - url = Uri.parse("https://localhost/api.php"), + url = "http://localhost/api.php", name = "PCM", baseFolderName = "pcm", ), Server( - url = Uri.parse("https://localhost/api.php"), + url = "http://localhost/api.php", name = "PCM2", baseFolderName = "pcm2", ) ) ) - val currentPath: LiveData = MutableLiveData("") + private val _currentServer: MutableLiveData = MutableLiveData(null) + val currentServer: LiveData = _currentServer; + fun setCurrentServer(srv: Server) { + _currentServer.postValue(srv) + } - val drawerFolders: LiveData> = MutableLiveData( + private val _currentPath: MutableLiveData = MutableLiveData("") + val currentPath: LiveData = _currentPath + + private val _drawerFolders: MutableLiveData> = MutableLiveData( listOf( Folder("Test/"), Folder("Folder/") ) ) + val drawerFolders: LiveData> = _drawerFolders - val folders: LiveData> = MutableLiveData( + private val _folders: MutableLiveData> = MutableLiveData( listOf( Folder("Test/Converted-Modules/") ) ) + val folders: LiveData> = _folders - val songs: LiveData> = MutableLiveData( + private val _songs: MutableLiveData> = MutableLiveData( listOf( Song( filePath = "Test/Rick Astley - Never Gonna Give You Up.mp3", - url = Uri.parse("http://localhost/rickroll.mp3") + url = "http://localhost/rickroll.mp3" ), Song( filePath = "Test/哇 好东西一堆.xm", - url = Uri.parse("http://localhost/rickroll.mp3") + url = "http://localhost/rickroll.mp3" ) ) ) + val songs: LiveData> = _songs 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) { - // + 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) + } } } \ No newline at end of file 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 569970b..a68c74a 100644 --- a/app/src/main/java/net/blumia/pcmdroid/model/Server.kt +++ b/app/src/main/java/net/blumia/pcmdroid/model/Server.kt @@ -1,9 +1,7 @@ package net.blumia.pcmdroid.model -import android.net.Uri - data class Server ( - val url: Uri, + val url: String, val name: String, val baseFolderName: String, ) diff --git a/app/src/main/java/net/blumia/pcmdroid/model/Song.kt b/app/src/main/java/net/blumia/pcmdroid/model/Song.kt index 868eee0..a322f86 100644 --- a/app/src/main/java/net/blumia/pcmdroid/model/Song.kt +++ b/app/src/main/java/net/blumia/pcmdroid/model/Song.kt @@ -1,13 +1,16 @@ package net.blumia.pcmdroid.model import android.net.Uri +import org.json.JSONException +import org.json.JSONObject +import java.net.URLDecoder data class Song ( val filePath: String, // with file name - val modifiedTime: Int = 0, + val modifiedTime: Long = 0, val fileSize: Int = 0, val additionalInfo: Boolean = false, // if sidecar meta-info json file exist. - val url: Uri, + val url: String, ) { fun displayName(): String { return Uri.parse(filePath).lastPathSegment ?: "???" @@ -18,6 +21,41 @@ data class Folder ( val folderPath: String, ) { fun displayName(): String { - return Uri.parse(folderPath).lastPathSegment ?: "???" + return URLDecoder.decode(Uri.parse(folderPath).lastPathSegment ?: "???", "UTF-8") } +} + +fun parseFromJsonString(jsonString: String): Pair, List> { + val folders = mutableListOf() + val songs = mutableListOf() + + 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) } \ 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 38b710c..db5b898 100644 --- a/app/src/main/java/net/blumia/pcmdroid/ui/NavGraph.kt +++ b/app/src/main/java/net/blumia/pcmdroid/ui/NavGraph.kt @@ -1,11 +1,16 @@ package net.blumia.pcmdroid.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController 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.main.MainPlayer import net.blumia.pcmdroid.ui.screen.settings.SettingsScreen @@ -27,11 +32,26 @@ fun NavGraph( startDestination = startDestination ) { composable(MainDestinations.MAIN_ROUTE) { + + val servers: List by viewModel.servers.observeAsState(initial = listOf()) + val drawerFolders: List by viewModel.drawerFolders.observeAsState(initial = listOf()) + val folders: List by viewModel.folders.observeAsState(listOf()) + val songs: List by viewModel.songs.observeAsState(listOf()) + MainPlayer( - viewModel = viewModel, + servers = servers, + drawerFolders = drawerFolders, + folders = folders, + songs = songs, addServerActionTriggered = { navController.navigate(MainDestinations.ADD_SERVER_ROUTE) }, + onServerItemIconClicked = { server -> + viewModel.fetchServer(server) + }, + onDrawerFolderItemClicked = { folder -> + viewModel.fetchFolder(folder) + }, settingsActionTriggered = { navController.navigate(MainDestinations.SETTINGS_ROUTE) } diff --git a/app/src/main/java/net/blumia/pcmdroid/ui/screen/main/MainPlayer.kt b/app/src/main/java/net/blumia/pcmdroid/ui/screen/main/MainPlayer.kt index 0a72826..8224673 100644 --- a/app/src/main/java/net/blumia/pcmdroid/ui/screen/main/MainPlayer.kt +++ b/app/src/main/java/net/blumia/pcmdroid/ui/screen/main/MainPlayer.kt @@ -1,12 +1,9 @@ package net.blumia.pcmdroid.ui.screen.main import android.net.Uri -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.material.* import androidx.compose.material.icons.Icons 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.PlayCircleOutline 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.Modifier 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.unit.dp import kotlinx.coroutines.launch -import net.blumia.pcmdroid.MainViewModel import net.blumia.pcmdroid.R import net.blumia.pcmdroid.model.Folder import net.blumia.pcmdroid.model.Server @@ -74,15 +73,17 @@ fun NowPlaying() { @OptIn(ExperimentalMaterialApi::class) @Composable fun DrawerContent( - viewModel: MainViewModel = MainViewModel(), + servers: List = listOf(), + drawerFolders: List = listOf(), addServerActionTriggered: () -> Unit = {}, + onServerItemIconClicked: (Server) -> Unit = {}, + onDrawerFolderItemClicked: (Folder) -> Unit = {}, settingsActionTriggered: () -> Unit = {}, ) { Row(modifier = Modifier.fillMaxSize()) { var selectedItem by remember { mutableStateOf(0) } - val servers = viewModel.servers.value NavigationRail { - servers?.forEachIndexed { index, server -> + servers.forEachIndexed { index, server -> NavigationRailItem( icon = { Icon( @@ -94,7 +95,7 @@ fun DrawerContent( }, selected = selectedItem == index, onClick = { - viewModel.fetchServer(server) + onServerItemIconClicked(server) } ) } @@ -133,11 +134,10 @@ fun DrawerContent( Divider() - val drawerFolders = viewModel.drawerFolders.value - drawerFolders?.forEach { folder -> + drawerFolders.forEach { folder -> ListItem( modifier = Modifier.clickable{ - viewModel.fetchFolder(folder) + onDrawerFolderItemClicked(folder) }, text = { Text( @@ -155,8 +155,13 @@ fun DrawerContent( @OptIn(ExperimentalMaterialApi::class) @Composable fun MainPlayer( - viewModel: MainViewModel = MainViewModel(), + servers: List = listOf(), + drawerFolders: List = listOf(), + folders: List = listOf(), + songs: List = listOf(), addServerActionTriggered: () -> Unit = {}, + onServerItemIconClicked: (Server) -> Unit = {}, + onDrawerFolderItemClicked: (Folder) -> Unit = {}, settingsActionTriggered: () -> Unit = {}, ) { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) @@ -165,8 +170,11 @@ fun MainPlayer( drawerState = drawerState, drawerContent = { DrawerContent( - viewModel = viewModel, + servers = servers, + drawerFolders = drawerFolders, addServerActionTriggered = addServerActionTriggered, + onServerItemIconClicked = onServerItemIconClicked, + onDrawerFolderItemClicked = onDrawerFolderItemClicked, settingsActionTriggered = settingsActionTriggered, ) } @@ -195,7 +203,11 @@ fun MainPlayer( Column( modifier = Modifier.fillMaxSize() ) { - Column(modifier = Modifier.weight(1f, fill = true)) { + Column( + modifier = Modifier + .weight(1f, fill = true) + .verticalScroll(rememberScrollState()) + ) { val iconLeftPadding = 16.dp val iconVerticalPadding = 12.dp @@ -229,7 +241,7 @@ fun MainPlayer( } } - viewModel.folders.value?.forEach { folder -> + folders.forEach { folder -> ListItem( modifier = Modifier.clickable { }, icon = { @@ -244,7 +256,7 @@ fun MainPlayer( ) } - viewModel.songs.value?.forEach { song -> + songs.forEach { song -> ListItem( modifier = Modifier.clickable { }, icon = {