diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 8cc9f7e20..639e477d7 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -56,6 +56,13 @@ dependencies {
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.8.2")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
+
+ implementation("com.github.alorma:compose-settings-ui-m3:1.0.3")
+ implementation("com.github.alorma:compose-settings-storage-preferences:1.0.3")
+ implementation("com.google.accompanist:accompanist-permissions:0.33.2-alpha")
// Android Studio Preview support
implementation("androidx.compose.ui:ui-tooling-preview")
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7dc120a0f..f5d6d6d8b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -24,16 +24,19 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
- android:roundIcon="@mipmap/ic_launcher_round">
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:theme="@style/Theme.MPD">
+
+
diff --git a/android/app/src/main/java/org/musicpd/Main.java b/android/app/src/main/java/org/musicpd/Main.java
index e943a6089..7196e0c5f 100644
--- a/android/app/src/main/java/org/musicpd/Main.java
+++ b/android/app/src/main/java/org/musicpd/Main.java
@@ -25,6 +25,8 @@ import android.widget.RemoteViews;
import androidx.core.app.ServiceCompat;
+import org.musicpd.ui.SettingsActivity;
+
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
@@ -197,7 +199,7 @@ public class Main extends Service implements Runnable {
}
}, filter);
- final Intent mainIntent = new Intent(this, Settings.class);
+ final Intent mainIntent = new Intent(this, SettingsActivity.class);
mainIntent.setAction("android.intent.action.MAIN");
mainIntent.addCategory("android.intent.category.LAUNCHER");
final PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
diff --git a/android/app/src/main/java/org/musicpd/NetworkUtil.java b/android/app/src/main/java/org/musicpd/NetworkUtil.java
index 2579f2d51..7f664ca39 100644
--- a/android/app/src/main/java/org/musicpd/NetworkUtil.java
+++ b/android/app/src/main/java/org/musicpd/NetworkUtil.java
@@ -5,12 +5,15 @@ import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
+import androidx.annotation.Nullable;
+
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.List;
public class NetworkUtil {
+ @Nullable
public static String getDeviceIPV4Address(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
LinkProperties linkProperties = connectivityManager.getLinkProperties(connectivityManager.getActiveNetwork());
diff --git a/android/app/src/main/java/org/musicpd/Settings.java b/android/app/src/main/java/org/musicpd/Settings.java
deleted file mode 100644
index 4d035f9e5..000000000
--- a/android/app/src/main/java/org/musicpd/Settings.java
+++ /dev/null
@@ -1,267 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-// Copyright The Music Player Daemon Project
-
-package org.musicpd;
-
-import java.util.LinkedList;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.util.Log;
-import android.widget.ArrayAdapter;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.CompoundButton.OnCheckedChangeListener;
-import android.widget.ListView;
-import android.widget.TextView;
-import android.widget.ToggleButton;
-
-public class Settings extends Activity {
- private static final String TAG = "Settings";
- private Main.Client mClient;
- private TextView mTextStatus;
- private ToggleButton mRunButton;
- private boolean mFirstRun;
- private LinkedList mLogListArray = new LinkedList();
- private ListView mLogListView;
- private ArrayAdapter mLogListAdapter;
-
- private static final int MAX_LOGS = 500;
-
- private static final int MSG_ERROR = 0;
- private static final int MSG_STOPPED = 1;
- private static final int MSG_STARTED = 2;
- private static final int MSG_LOG = 3;
-
- private Handler mHandler = new Handler(new Handler.Callback() {
- @Override
- public boolean handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_ERROR:
- Log.d(TAG, "onError");
-
- mClient.release();
- connectClient();
-
- mRunButton.setEnabled(false);
- mRunButton.setChecked(false);
-
- mTextStatus.setText((String)msg.obj);
- mFirstRun = true;
- break;
- case MSG_STOPPED:
- Log.d(TAG, "onStopped");
- mRunButton.setEnabled(true);
- if (!mFirstRun && Preferences.getBoolean(Settings.this, Preferences.KEY_RUN_ON_BOOT, false))
- mRunButton.setChecked(true);
- else
- mRunButton.setChecked(false);
- mFirstRun = true;
- mTextStatus.setText("");
- break;
- case MSG_STARTED:
- Log.d(TAG, "onStarted");
- mRunButton.setChecked(true);
- mFirstRun = true;
- mTextStatus.setText("MPD service started");
- break;
- case MSG_LOG:
- if (mLogListArray.size() > MAX_LOGS)
- mLogListArray.remove(0);
- String priority;
- switch (msg.arg1) {
- case Log.DEBUG:
- priority = "D";
- break;
- case Log.ERROR:
- priority = "E";
- break;
- case Log.INFO:
- priority = "I";
- break;
- case Log.VERBOSE:
- priority = "V";
- break;
- case Log.WARN:
- priority = "W";
- break;
- default:
- priority = "";
- }
- mLogListArray.add(priority + "/ " + (String)msg.obj);
- mLogListAdapter.notifyDataSetChanged();
-
- break;
- }
- return true;
- }
- });
-
- private final OnCheckedChangeListener mOnRunChangeListener = new OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- if (mClient != null) {
- if (isChecked) {
- mClient.start();
- if (Preferences.getBoolean(Settings.this,
- Preferences.KEY_WAKELOCK, false))
- mClient.setWakelockEnabled(true);
- if (Preferences.getBoolean(Settings.this,
- Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, false))
- mClient.setPauseOnHeadphonesDisconnect(true);
- } else {
- mClient.stop();
- }
- }
- }
- };
-
- private final OnCheckedChangeListener mOnRunOnBootChangeListener = new OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- Preferences.putBoolean(Settings.this, Preferences.KEY_RUN_ON_BOOT, isChecked);
- if (isChecked && mClient != null && !mRunButton.isChecked())
- mRunButton.setChecked(true);
- }
- };
-
- private final OnCheckedChangeListener mOnWakelockChangeListener = new OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- Preferences.putBoolean(Settings.this, Preferences.KEY_WAKELOCK, isChecked);
- if (mClient != null && mClient.isRunning())
- mClient.setWakelockEnabled(isChecked);
- }
- };
-
- private final OnCheckedChangeListener mOnPauseOnHeadphonesDisconnectChangeListener = new OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- Preferences.putBoolean(Settings.this, Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, isChecked);
- if (mClient != null && mClient.isRunning())
- mClient.setPauseOnHeadphonesDisconnect(isChecked);
- }
- };
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- /* TODO: this sure is the wrong place to request
- permissions - it will cause MPD to quit
- immediately; we should request permissions when we
- need them, but implementing that is complicated, so
- for now, we do it here to give users a quick
- solution for the problem */
- requestAllPermissions();
-
- setContentView(R.layout.settings);
- mRunButton = (ToggleButton) findViewById(R.id.run);
- mRunButton.setOnCheckedChangeListener(mOnRunChangeListener);
-
- mTextStatus = (TextView) findViewById(R.id.status);
-
- mLogListAdapter = new ArrayAdapter(this, R.layout.log_item, mLogListArray);
-
- mLogListView = (ListView) findViewById(R.id.log_list);
- mLogListView.setAdapter(mLogListAdapter);
- mLogListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
-
- CheckBox checkbox = (CheckBox) findViewById(R.id.run_on_boot);
- checkbox.setOnCheckedChangeListener(mOnRunOnBootChangeListener);
- if (Preferences.getBoolean(this, Preferences.KEY_RUN_ON_BOOT, false))
- checkbox.setChecked(true);
-
- checkbox = (CheckBox) findViewById(R.id.wakelock);
- checkbox.setOnCheckedChangeListener(mOnWakelockChangeListener);
- if (Preferences.getBoolean(this, Preferences.KEY_WAKELOCK, false))
- checkbox.setChecked(true);
-
- checkbox = (CheckBox) findViewById(R.id.pause_on_headphones_disconnect);
- checkbox.setOnCheckedChangeListener(mOnPauseOnHeadphonesDisconnectChangeListener);
- if (Preferences.getBoolean(this, Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, false))
- checkbox.setChecked(true);
-
- TextView networkAddressTextView = (TextView) findViewById(R.id.networkAddress);
- String deviceIPV4Address = NetworkUtil.getDeviceIPV4Address(this);
- networkAddressTextView.setText(deviceIPV4Address);
-
- super.onCreate(savedInstanceState);
- }
-
- private void checkRequestPermission(String permission) {
- if (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
- return;
-
- try {
- this.requestPermissions(new String[]{permission}, 0);
- } catch (Exception e) {
- Log.e(TAG, "requestPermissions(" + permission + ") failed",
- e);
- }
- }
-
- private void requestAllPermissions() {
- if (android.os.Build.VERSION.SDK_INT < 23)
- /* we don't need to request permissions on
- this old Android version */
- return;
-
- /* starting with Android 6.0, we need to explicitly
- request all permissions before using them;
- mentioning them in the manifest is not enough */
-
- checkRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE);
- }
-
- private void connectClient() {
- mClient = new Main.Client(this, new Main.Client.Callback() {
-
- private void removeMessages() {
- /* don't remove log messages */
- mHandler.removeMessages(MSG_STOPPED);
- mHandler.removeMessages(MSG_STARTED);
- mHandler.removeMessages(MSG_ERROR);
- }
-
- @Override
- public void onStopped() {
- removeMessages();
- mHandler.sendEmptyMessage(MSG_STOPPED);
- }
-
- @Override
- public void onStarted() {
- removeMessages();
- mHandler.sendEmptyMessage(MSG_STARTED);
- }
-
- @Override
- public void onError(String error) {
- removeMessages();
- mHandler.sendMessage(Message.obtain(mHandler, MSG_ERROR, error));
- }
-
- @Override
- public void onLog(int priority, String msg) {
- mHandler.sendMessage(Message.obtain(mHandler, MSG_LOG, priority, 0, msg));
- }
- });
- }
-
- @Override
- protected void onStart() {
- mFirstRun = false;
- connectClient();
- super.onStart();
- }
-
- @Override
- protected void onStop() {
- mClient.release();
- mClient = null;
- super.onStop();
- }
-}
diff --git a/android/app/src/main/java/org/musicpd/ui/NetworkAddress.kt b/android/app/src/main/java/org/musicpd/ui/NetworkAddress.kt
new file mode 100644
index 000000000..fd387ca6a
--- /dev/null
+++ b/android/app/src/main/java/org/musicpd/ui/NetworkAddress.kt
@@ -0,0 +1,39 @@
+package org.musicpd.ui
+
+import android.app.Application
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Wifi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import org.musicpd.NetworkUtil
+
+@Composable
+fun NetworkAddress() {
+ val address = NetworkUtil.getDeviceIPV4Address(LocalContext.current)
+ val padding = 4.dp
+ Row(
+ Modifier
+ .padding(padding)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Wifi,
+ contentDescription = "Wifi")
+ Spacer(Modifier.size(padding))
+ Text(text = address ?: "")
+ }
+}
+
diff --git a/android/app/src/main/java/org/musicpd/ui/Preferences.kt b/android/app/src/main/java/org/musicpd/ui/Preferences.kt
new file mode 100644
index 000000000..da357559d
--- /dev/null
+++ b/android/app/src/main/java/org/musicpd/ui/Preferences.kt
@@ -0,0 +1,52 @@
+package org.musicpd.ui
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BatteryAlert
+import androidx.compose.material.icons.filled.Headphones
+import androidx.compose.material.icons.filled.PowerSettingsNew
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.alorma.compose.settings.storage.preferences.rememberPreferenceBooleanSettingState
+import com.alorma.compose.settings.ui.SettingsSwitch
+import org.musicpd.Preferences
+import org.musicpd.R
+
+@Composable
+fun SettingsOptions(
+ onBootChanged: (Boolean) -> Unit,
+ onWakeLockChanged: (Boolean) -> Unit,
+ onHeadphonesChanged: (Boolean) -> Unit
+) {
+ val bootState = rememberPreferenceBooleanSettingState(
+ key = Preferences.KEY_RUN_ON_BOOT,
+ defaultValue = false
+ )
+ val wakelockState =
+ rememberPreferenceBooleanSettingState(key = Preferences.KEY_WAKELOCK, defaultValue = false)
+ val headphoneState = rememberPreferenceBooleanSettingState(
+ key = Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT,
+ defaultValue = false
+ )
+
+ SettingsSwitch(
+ icon = { Icon(imageVector = Icons.Default.PowerSettingsNew, contentDescription = "Power") },
+ title = { Text(text = stringResource(R.string.checkbox_run_on_boot)) },
+ onCheckedChange = onBootChanged,
+ state = bootState
+ )
+ SettingsSwitch(
+ icon = { Icon(imageVector = Icons.Default.BatteryAlert, contentDescription = "Battery") },
+ title = { Text(text = stringResource(R.string.checkbox_wakelock)) },
+ onCheckedChange = onWakeLockChanged,
+ state = wakelockState
+ )
+ SettingsSwitch(
+ icon = { Icon(imageVector = Icons.Default.Headphones, contentDescription = "Headphones") },
+ title = { Text(text = stringResource(R.string.checkbox_pause_on_headphones_disconnect)) },
+ onCheckedChange = onHeadphonesChanged,
+ state = headphoneState
+ )
+
+}
diff --git a/android/app/src/main/java/org/musicpd/ui/SettingsActivity.kt b/android/app/src/main/java/org/musicpd/ui/SettingsActivity.kt
new file mode 100644
index 000000000..56e4b0f92
--- /dev/null
+++ b/android/app/src/main/java/org/musicpd/ui/SettingsActivity.kt
@@ -0,0 +1,212 @@
+package org.musicpd.ui
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Circle
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.rememberPermissionState
+import com.google.accompanist.permissions.shouldShowRationale
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.musicpd.Main
+import org.musicpd.R
+
+class SettingsActivity : ComponentActivity() {
+
+ private val settingsViewModel: SettingsViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MaterialTheme {
+ SettingsContainer(settingsViewModel)
+ }
+ }
+ }
+
+ private fun connectClient() {
+ val client = Main.Client(this, object : Main.Client.Callback {
+ override fun onStopped() {
+ settingsViewModel.updateStatus("", false)
+ }
+
+ override fun onStarted() {
+ settingsViewModel.updateStatus("MPD Service Started", true)
+ }
+
+ override fun onError(error: String) {
+ settingsViewModel.removeClient()
+ settingsViewModel.updateStatus(error, false)
+ connectClient()
+ }
+
+ override fun onLog(priority: Int, msg: String) {
+ settingsViewModel.addLogItem(priority, msg)
+ }
+ })
+
+ settingsViewModel.setClient(client)
+ }
+
+ override fun onStart() {
+ //mFirstRun = false
+ connectClient()
+ super.onStart()
+ }
+
+ override fun onStop() {
+ settingsViewModel.removeClient()
+ super.onStop()
+ }
+}
+
+@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun SettingsContainer(settingsViewModel: SettingsViewModel = viewModel()) {
+ val context = LocalContext.current
+
+ val storagePermissionState = rememberPermissionState(
+ android.Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+
+ if (storagePermissionState.status.shouldShowRationale) {
+ Column(Modifier
+ .padding(4.dp)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(stringResource(id = R.string.external_files_permission_request))
+ Button(onClick = { }) {
+ Text("Request permission")
+ }
+ }
+ } else {
+ Column {
+ NetworkAddress()
+ ServerStatus(settingsViewModel)
+ if (!storagePermissionState.status.isGranted) {
+ OutlinedButton(onClick = { storagePermissionState.launchPermissionRequest() }, Modifier
+ .padding(4.dp)
+ .fillMaxWidth()) {
+ Text("Request external storage permission", color = MaterialTheme.colorScheme.secondary)
+ }
+ }
+ SettingsOptions(
+ onBootChanged = { newValue ->
+ if (newValue) {
+ settingsViewModel.startMPD(context)
+ }
+ },
+ onWakeLockChanged = { newValue ->
+ settingsViewModel.setWakelockEnabled(newValue)
+ },
+ onHeadphonesChanged = { newValue ->
+ settingsViewModel.setPauseOnHeadphonesDisconnect(newValue)
+ }
+ )
+ LogView(settingsViewModel.logItemFLow.collectAsStateWithLifecycle())
+ }
+ }
+}
+
+@Composable
+fun ServerStatus(settingsViewModel: SettingsViewModel) {
+ val context = LocalContext.current
+
+ val statusUiState by settingsViewModel.statusUIState.collectAsState()
+
+ Column {
+ Row(
+ Modifier
+ .padding(4.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ Row {
+ Icon(
+ imageVector = Icons.Default.Circle,
+ contentDescription = "",
+ tint = if (statusUiState.running) Color(0xFFB8F397) else Color(0xFFFFDAD6)
+ )
+ Text(text = if (statusUiState.running) "Running" else "Stopped")
+ }
+ Button(onClick = {
+ if (statusUiState.running)
+ settingsViewModel.stopMPD()
+ else
+ settingsViewModel.startMPD(context)
+ }) {
+ Text(text = if (statusUiState.running) "Stop MPD" else "Start MPD")
+ }
+ }
+ Row(
+ Modifier
+ .padding(4.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ Text(text = statusUiState.statusMessage)
+ }
+ }
+}
+
+@Composable
+fun LogView(messages: State>) {
+ val state = rememberLazyListState()
+
+ LazyColumn(
+ Modifier.padding(4.dp),
+ state
+ ) {
+ items(messages.value) { message ->
+ Text(text = message, fontFamily = FontFamily.Monospace)
+ }
+ CoroutineScope(Dispatchers.Main).launch {
+ state.scrollToItem(messages.value.count(), 0)
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun SettingsPreview() {
+ MaterialTheme {
+ SettingsContainer()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt b/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt
new file mode 100644
index 000000000..3d09370ee
--- /dev/null
+++ b/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt
@@ -0,0 +1,84 @@
+package org.musicpd.ui
+
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import org.musicpd.Main
+import org.musicpd.Preferences
+
+private const val MAX_LOGS = 500
+
+class SettingsViewModel : ViewModel() {
+
+ private var mClient: Main.Client? = null
+
+ private val _logItemFLow = MutableStateFlow(listOf())
+ val logItemFLow: StateFlow> = _logItemFLow
+
+ data class StatusUiState(
+ val statusMessage: String = "",
+ val running: Boolean = false
+ )
+
+ private val _statusUIState = MutableStateFlow(StatusUiState())
+ val statusUIState: StateFlow = _statusUIState.asStateFlow()
+
+ fun addLogItem(priority: Int, message: String) {
+ if (_logItemFLow.value.size > MAX_LOGS) {
+ _logItemFLow.value = _logItemFLow.value.drop(1)
+ }
+
+ val priorityString: String = when (priority) {
+ Log.DEBUG -> "D"
+ Log.ERROR -> "E"
+ Log.INFO -> "I"
+ Log.VERBOSE -> "V"
+ Log.WARN -> "W"
+ else -> ""
+ }
+
+ _logItemFLow.value = _logItemFLow.value + ("$priorityString/$message")
+ }
+
+ fun updateStatus(message: String, running: Boolean) {
+ _statusUIState.value = StatusUiState(message, running)
+ }
+
+ fun setClient(client: Main.Client) {
+ mClient = client
+ }
+
+ fun removeClient() {
+ mClient?.release()
+ mClient = null
+ }
+
+ fun startMPD(context: Context) {
+ mClient?.start()
+ if (Preferences.getBoolean(
+ context,
+ Preferences.KEY_WAKELOCK, false
+ )
+ ) mClient?.setWakelockEnabled(true)
+ if (Preferences.getBoolean(
+ context,
+ Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, false
+ )
+ ) mClient?.setPauseOnHeadphonesDisconnect(true)
+ }
+
+ fun stopMPD() {
+ mClient?.stop()
+ }
+
+ fun setWakelockEnabled(enabled: Boolean) {
+ mClient?.setWakelockEnabled(enabled)
+ }
+
+ fun setPauseOnHeadphonesDisconnect(enabled: Boolean) {
+ mClient?.setPauseOnHeadphonesDisconnect(enabled)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/log_item.xml b/android/app/src/main/res/layout/log_item.xml
deleted file mode 100644
index e6e74c913..000000000
--- a/android/app/src/main/res/layout/log_item.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
diff --git a/android/app/src/main/res/layout/settings.xml b/android/app/src/main/res/layout/settings.xml
deleted file mode 100644
index c7c6c400b..000000000
--- a/android/app/src/main/res/layout/settings.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 2df00470e..92f8139b2 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -9,4 +9,5 @@
Run MPD automatically on boot
Prevent suspend when MPD is running (Wakelock)
Pause MPD when headphones disconnect
+ MPD requires access to external files to play local music. Please grant the permission.
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..c9502c8f8
--- /dev/null
+++ b/android/app/src/main/res/values/themes.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file