fix livedata not updated, playlist not scrollable
This commit is contained in:
		@ -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'
 | 
			
		||||
 | 
			
		||||
@ -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<List<Server>> = MutableLiveData<List<Server>>(
 | 
			
		||||
        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<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(
 | 
			
		||||
            Folder("Test/"),
 | 
			
		||||
            Folder("Folder/")
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    val drawerFolders: LiveData<List<Folder>> = _drawerFolders
 | 
			
		||||
 | 
			
		||||
    val folders: LiveData<List<Folder>> = MutableLiveData(
 | 
			
		||||
    private val _folders: MutableLiveData<List<Folder>> = MutableLiveData(
 | 
			
		||||
        listOf(
 | 
			
		||||
            Folder("Test/Converted-Modules/")
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    val folders: LiveData<List<Folder>> = _folders
 | 
			
		||||
 | 
			
		||||
    val songs: LiveData<List<Song>> = MutableLiveData(
 | 
			
		||||
    private val _songs: MutableLiveData<List<Song>> = 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<List<Song>> = _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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -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<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)
 | 
			
		||||
}
 | 
			
		||||
@ -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<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(
 | 
			
		||||
                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)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -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<Server> = listOf(),
 | 
			
		||||
    drawerFolders: List<Folder> = listOf(),
 | 
			
		||||
    addServerActionTriggered: () -> Unit = {},
 | 
			
		||||
    onServerItemIconClicked: (Server) -> Unit = {},
 | 
			
		||||
    onDrawerFolderItemClicked: (Folder) -> Unit = {},
 | 
			
		||||
    settingsActionTriggered: () -> Unit = {},
 | 
			
		||||
) {
 | 
			
		||||
    Row(modifier = Modifier.fillMaxSize()) {
 | 
			
		||||
        var selectedItem by remember { mutableStateOf<Int?>(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<Server> = listOf(),
 | 
			
		||||
    drawerFolders: List<Folder> = listOf(),
 | 
			
		||||
    folders: List<Folder> = listOf(),
 | 
			
		||||
    songs: List<Song> = 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 = {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user