diff --git a/app/src/main/java/com/example/findingwav/Exporter.kt b/app/src/main/java/com/example/findingwav/Exporter.kt new file mode 100644 index 0000000..f4ff035 --- /dev/null +++ b/app/src/main/java/com/example/findingwav/Exporter.kt @@ -0,0 +1,132 @@ +package com.example.findingwav + +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.app.DownloadManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import java.io.File +import java.io.OutputStream + +/** Writes to downloads using mediastore api */ +@RequiresApi(Build.VERSION_CODES.Q) +fun savePlaylistToDownloads(context: Context, playlistName: String, content: String) { + try { + val resolver = context.contentResolver + val fileName = "$playlistName.m3u" + + // 1. Setup the file details + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put(MediaStore.MediaColumns.MIME_TYPE, "audio/x-mpegurl") + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + + // 2. Ask MediaStore to create the file entry + // This works even on Android 11+ without special permissions + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + + if (uri != null) { + // 3. Open the stream and write the data + val outputStream: OutputStream? = resolver.openOutputStream(uri) + outputStream?.use { stream -> + stream.write(content.toByteArray()) + } + + // 4. Success! Show Toast + Toast.makeText(context, "Saved $fileName to Downloads", Toast.LENGTH_SHORT).show() + + // 5. Open Downloads App + openDownloadsFolder(context) + } else { + Toast.makeText(context, "Failed to create file", Toast.LENGTH_SHORT).show() + } + + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_LONG).show() + } +} + +// Helper to open the folder +fun openDownloadsFolder(context: Context) { + try { + val intent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + context.startActivity(intent) + } catch (e: Exception) { + // Fallback for some devices that don't support this intent + println("Could not open downloads: ${e.message}") + } +} + +fun testM3U() { + var testSong: MainActivity.Audio = MainActivity.Audio( + Uri.parse("Music/Aja - Steely Dan (320).mp3"), + "Music/ Steely Dan - Aja", + "Album", + "Aja", + "Steely Dan", + 480 + ) + + var playlist: MutableList = mutableListOf() + playlist.add(testSong) + + //println(toM3U("Main", playlist)) +} + + +/** + * Format is + * #EXTM3U *Initialiser* + * #EXTINF:RUNTIME(seconds),(noSpace)ARTIST_NAME - SONG NAME + * FILEPATH/FILENAME + * + * example: + * #EXTM3U + * #EXTINF:480,Steely Dan - Aja + * Music/Aja - Steely Dan (320).mp3 + * + * + * */ +@RequiresApi(Build.VERSION_CODES.Q) +@androidx.annotation.OptIn(UnstableApi::class) +fun toM3U(playlistName: String, playlist: MutableList?, context: Context) { + // grab a playlist + var out: StringBuilder = StringBuilder() + + out.append("#EXTM3U\n") + //val path = Environment.getExternalStoragePublicDirectory("Music" + val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + if (playlist != null) { + for (song in playlist) { + var metaData = song.mediaMetadata + // Using metaData.durationMS here would necessitate deprecated/experimental stuff + // But easier than bringing the music player all the way here + // + out.append("#EXTINF:").append(song.mediaMetadata.durationMs?.div(1000) ?: 1).append(",") + .append(metaData.artist.toString()) + .append(" - ") + // Title is the actual name of the song (maybe switch with display title) + .append(metaData.title).append("\n") + // Display title is the file name + out.append(path).append("/").append(song.mediaMetadata.displayTitle).append("\n") + } + } +// +// file.writeText(out.toString()) + savePlaylistToDownloads(context, playlistName, out.toString()) + + Toast.makeText(context, "Playlist saved successfully!", Toast.LENGTH_SHORT).show() + openDownloadsFolder(context) +} + diff --git a/app/src/main/java/com/example/findingwav/MainActivity.kt b/app/src/main/java/com/example/findingwav/MainActivity.kt index 7470cb3..f914a25 100644 --- a/app/src/main/java/com/example/findingwav/MainActivity.kt +++ b/app/src/main/java/com/example/findingwav/MainActivity.kt @@ -45,10 +45,18 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import android.content.ComponentName +// used to open m3u in downloads +import android.app.DownloadManager +import android.media.MediaScannerConnection +import android.app.Activity +import android.content.ContentValues +import java.io.OutputStream + class MainActivity : AppCompatActivity() { private var player: Player? = null // Use generic Player interface; NOT TO BE CONFUSED WITH PLAYER.KT. private var controllerFuture: ListenableFuture? = null private var musicPlayerWrapper: MusicPlayer? = null + private val CUTOFFTIME: Long = 60000 // Define what happens after the user clicks "Allow" or "Deny" private val requestMusicPermissionLauncher = registerForActivityResult( @@ -115,7 +123,13 @@ class MainActivity : AppCompatActivity() { val controller = controllerFuture?.get() if (controller != null) { - musicPlayerWrapper = MusicPlayer(controller) + // if music player alr exists, no need to wipe everything + if (musicPlayerWrapper == null) { + musicPlayerWrapper = MusicPlayer(controller) + } else { + musicPlayerWrapper?.player = controller + } + // Check if we have permission AND if we need to load music val hasPermission = ContextCompat.checkSelfPermission( @@ -224,7 +238,7 @@ class MainActivity : AppCompatActivity() { while (cursor.moveToNext()) { val isMusic = cursor.getString(music) // Check that file is music file - if (isMusic.isNotEmpty()) { + if (isMusic.isNotEmpty() && cursor.getLong(durationColumn) > CUTOFFTIME) { // Assign the values of the files to these val id = cursor.getLong(idColumn) val name = cursor.getString(nameColumn) @@ -392,87 +406,4 @@ fun TitlePreview() { } } -fun testM3U() { - var testSong: MainActivity.Audio = MainActivity.Audio( - Uri.parse("Music/Aja - Steely Dan (320).mp3"), - "Music/ Steely Dan - Aja", - "Album", - "Aja", - "Steely Dan", - 480 - ) - - var playlist: MutableList = mutableListOf() - playlist.add(testSong) - - //println(toM3U("Main", playlist)) -} - - -/**To be used to create the .m3u file into files. Maybe works. Needs to change some params*/ -// pass in playlistName -// context is applicationContext -fun createFile(playlistName: String, playlist: String, context: Context/*TODO: CHANGE THIS*/) -{ - // Request code for creating a PDF document. - //val path = context.getExternalFilesDir(null) - val path = Environment.getExternalStoragePublicDirectory("Music") - File(path, "$playlistName" + ".m3u").delete() - println("Path: " + path) - // TODO: Add name of playlist file - var playlistFile = File(path, "$playlistName" + ".m3u") - // TODO: actually put playlist content, try a forEach or idk - - playlistFile.writeText("$playlist") - -} - - -/** - * Format is - * #EXTM3U *Initialiser* - * #EXTINF:RUNTIME(seconds),(noSpace)ARTIST_NAME - SONG NAME - * FILEPATH/FILENAME - * - * example: - * #EXTM3U - * #EXTINF:480,Steely Dan - Aja - * Music/Aja - Steely Dan (320).mp3 - * - * - * */ -@androidx.annotation.OptIn(UnstableApi::class) -fun toM3U(playlistName: String, playlist: MutableList?, context: Context) : String { - // grab a playlist - var out: StringBuilder = StringBuilder() - - out.append("#EXTM3U\n") - val path = Environment.getExternalStoragePublicDirectory("Music") - if (playlist != null) { - for (song in playlist) { - var metaData = song.mediaMetadata - // Using metaData.durationMS here would necessitate deprecated/experimental stuff - // But easier than bringing the music player all the way here - // - out.append("#EXTINF:").append(song.mediaMetadata.durationMs?.div(1000) ?: 1).append(",") - .append(metaData.artist.toString()) - .append(" - ") - // Title is the actual name of the song (maybe switch with display title) - .append(metaData.title).append("\n") - // Display title is the file name - out.append(path).append("/").append(song.mediaMetadata.displayTitle).append("\n") - } - } - // attempt to write locally to downloads? -// val filePath: String = "Playlists/$playlistName" -// val file = File(filePath) -// -// file.writeText(out.toString()) - createFile(playlistName, out.toString(), context) - - - println("Line written successfully") - - return out.toString() -} \ No newline at end of file diff --git a/app/src/main/java/com/example/findingwav/Player.kt b/app/src/main/java/com/example/findingwav/Player.kt index 30cb65b..4a84ae4 100644 --- a/app/src/main/java/com/example/findingwav/Player.kt +++ b/app/src/main/java/com/example/findingwav/Player.kt @@ -21,7 +21,7 @@ public enum class NextOpts { * @param player the music player to be used * @param songs the songs to add to the music player to play */ -public class MusicPlayer(val player: Player, songs: List? = null) { +public class MusicPlayer(var player: Player, songs: List? = null) { private var exoSongList: MutableList = mutableListOf() private var songCount : Int = 0 @@ -136,6 +136,7 @@ public class MusicPlayer(val player: Player, songs: List? = null) { */ public fun setCurrentPlaylist(name: String) : Boolean { if (getPlaylist(name) != null) { + // only set if it exists currentPlaylist = getPlaylist(name)!! return true } diff --git a/app/src/main/java/com/example/findingwav/data/datamanager.kt b/app/src/main/java/com/example/findingwav/data/datamanager.kt index e1e06b7..e7ea70c 100644 --- a/app/src/main/java/com/example/findingwav/data/datamanager.kt +++ b/app/src/main/java/com/example/findingwav/data/datamanager.kt @@ -15,4 +15,6 @@ public var NECESSARY_PLAYTIME : Double = 0.9; /** * Whether adding multiple of the same song is allowed in the playlist */ -public var REPEAT_SONGS : Boolean = false \ No newline at end of file +public var REPEAT_SONGS : Boolean = false + +public var DEBUG : Boolean = true \ No newline at end of file diff --git a/app/src/main/java/com/example/findingwav/ui/screens/PlayerScreen.kt b/app/src/main/java/com/example/findingwav/ui/screens/PlayerScreen.kt index 704f4f9..d928590 100644 --- a/app/src/main/java/com/example/findingwav/ui/screens/PlayerScreen.kt +++ b/app/src/main/java/com/example/findingwav/ui/screens/PlayerScreen.kt @@ -3,6 +3,7 @@ package com.example.findingwav.ui.screens import android.content.Context import android.graphics.Bitmap import android.os.Build +import android.widget.Toast import androidx.annotation.OptIn import androidx.annotation.RequiresApi import androidx.compose.foundation.Image @@ -66,6 +67,7 @@ import androidx.media3.common.util.UnstableApi import com.example.findingwav.MusicPlayer import com.example.findingwav.NextOpts import com.example.findingwav.R +import com.example.findingwav.data.DEBUG import com.example.findingwav.toM3U import com.example.findingwav.ui.theme.FindingWavTheme import com.github.theapache64.twyper.SwipedOutDirection @@ -127,7 +129,7 @@ fun PlayerScreen(musicPlayer : MusicPlayer, context : Context) { }, musicPlayer.getPlaylists(), - selectPlaylist = { musicPlayer.setCurrentPlaylist(musicPlayer.getCurrentPlaylistName()) }, + selectPlaylist = { musicPlayer.setCurrentPlaylist(musicPlayer.getCurrentPlaylistName()) }, // this i feel like is a culprit; at this point you dont even need to pass it??? musicPlayer.getCurrentPlaylistName() ) } @@ -205,118 +207,111 @@ private fun SettingsSelect() { * selectPlaylist(): Function to select playlist based off name. * */ @Composable -private fun PlaylistSelect(playlists: MutableMap>, selectPlaylist: (String) -> Unit) { - // dropdown menu for playlist select - // Declaring a boolean value to store - // the expanded state of the Text Field +private fun PlaylistSelect( + playlists: MutableMap>, + selectPlaylist: (String) -> Unit, + onCreateClicked: () -> Unit // <--- Callback to open the dialog +) { var mExpanded by remember { mutableStateOf(false) } + val mPlaylist = playlists.keys.toList() - // Create a list of cities - - val mPlaylist = playlists.keys - - // Create a string value to store the selected city - var mSelectedText by remember { mutableStateOf("") } - - var mTextFieldSize by remember { mutableStateOf(Size.Zero)} + // Default text logic + var mSelectedText by remember { mutableStateOf("Select Playlist") } - var showCreation by remember { - mutableStateOf(false) - } + var mTextFieldSize by remember { mutableStateOf(Size.Zero) } - // Up Icon when expanded and down icon when collapsed - val icon = if (mExpanded) - Icons.Filled.KeyboardArrowUp - else - Icons.Filled.KeyboardArrowDown + val icon = if (mExpanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown - Column(Modifier.padding(horizontal = 20.dp)) { + Column(modifier = Modifier.padding(horizontal = 20.dp)) { - // Create an Outlined Text Field - // with icon and not expanded + // The Dropdown Trigger (Text Field) OutlinedTextField( value = mSelectedText, onValueChange = { mSelectedText = it }, modifier = Modifier .fillMaxWidth() .onGloballyPositioned { coordinates -> - // This value is used to assign to - // the DropDown the same width mTextFieldSize = coordinates.size.toSize() }, - label = {Text("Playlist")}, + label = { Text("Playlist") }, trailingIcon = { - Icon(icon,"contentDescription", + Icon(icon, "contentDescription", Modifier.clickable { mExpanded = !mExpanded }) }, readOnly = true ) - // Create a drop-down menu with list of cities, - // when clicked, set the Text Field text as the city selected + // The Menu itself DropdownMenu( expanded = mExpanded, onDismissRequest = { mExpanded = false }, - modifier = Modifier - .width(with(LocalDensity.current){mTextFieldSize.width.toDp()}) + modifier = Modifier.width(with(LocalDensity.current) { mTextFieldSize.width.toDp() }) ) { + // Existing Playlists mPlaylist.forEach { label -> - DropdownMenuItem(onClick = { - mSelectedText = label - // set playlist (current playlist) - selectPlaylist(label) - mExpanded = false - }, - text = { Text(text = label) } + DropdownMenuItem( + text = { Text(text = label) }, + onClick = { + mSelectedText = label + selectPlaylist(label) + mExpanded = false + } ) } - // create new playlist button - DropdownMenuItem(text = { Text(text = "Create New Playlist") }, onClick = { showCreation = true }) + // create new thingy + DropdownMenuItem( + text = { + Text( + text = "Create New Playlist", + color = MaterialTheme.colorScheme.primary // Optional: Make it stand out + ) + }, + onClick = { + mExpanded = false // Close menu first + onCreateClicked() // Then open dialog + } + ) } } - - /** Prompt the user to enter text and create a new playlist - * Holy hell I am tired - */ - if (showCreation) { - - } } +/** Popup that displays when creating a new playlist. */ // https://stackoverflow.com/questions/73455840/textfield-new-line-issue-in-alert-dialog-with-jetpack-compose @Composable -private fun CreatePlaylistAlert() { - var showCreation by remember { - mutableStateOf(false) - } - +fun CreatePlaylistAlert( + onDismiss: () -> Unit, + onConfirm: (String) -> Unit +) { val text = remember { mutableStateOf("") } - val textLength = remember { mutableStateOf(0) } AlertDialog( - onDismissRequest = { showCreation = false }, - title = { - Text(text = "Create new playlist?") + onDismissRequest = onDismiss, + title = { Text(text = "New Playlist Name") }, + text = { + TextField( + value = text.value, + onValueChange = { if (it.length <= 200) text.value = it }, + singleLine = true + ) }, - text = { TextField( - value = text.value, - onValueChange = { - if (it.length > 200) { - textLength.value = it.length - text.value = it + confirmButton = { + Button( + onClick = { + if (text.value.isNotBlank()) { + onConfirm(text.value) + } } - }, - )}, - confirmButton = { Button(onClick = { showCreation = false;}) - { - // This is the text of the button - Text(text = "Add Playlist") - } + ) { + Text("Create") + } }, - - - ) + dismissButton = { + Button(onClick = onDismiss) { + Text("Cancel") + } + } + ) } @@ -384,12 +379,15 @@ private fun Player( } + var showCreateDialog by remember { mutableStateOf(false) } + // main body thingy Column ( modifier = Modifier.padding(top = 110.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // Playlist selector - PlaylistSelect(playlists, selectPlaylist = {selectPlaylist(currentPlaylistName)}) + PlaylistSelect(playlists, selectPlaylist = {selectPlaylist(currentPlaylistName)}, + onCreateClicked = { showCreateDialog = true }) // song title (replace with song name variable SongTitle(title = currentSongMetadata.value.title.toString()) @@ -507,7 +505,25 @@ private fun Player( }) } - + // popup + // probably should separate this composable but oh well + if (showCreateDialog) { + CreatePlaylistAlert( + onDismiss = { showCreateDialog = false }, + onConfirm = { newName -> + // Create the playlist in your music player + musicPlayer.addPlaylist(newName, replace = false) + + // Switch to it immediately (optional) + musicPlayer.setCurrentPlaylist(newName) + if (DEBUG) { + Toast.makeText(context, musicPlayer.getCurrentPlaylistName(), Toast.LENGTH_SHORT).show() + } + // Close dialog + showCreateDialog = false + } + ) + } } @Composable