diff --git a/README.md b/README.md index 245d1cd82..97173d2f7 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,34 @@ -# Phonograph +# Phonograph ([Forked from kabouzeid](https://github.com/kabouzeid/Phonograph)) + [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://github.com/kabouzeid/Phonograph/blob/master/LICENSE.txt) **A material designed local music player for Android.** -![Screenshots](./art/art.jpg?raw=true) +## About this fork +The purpose of this fork is to implement an easy to use and powerful way to manage your playlists. The goals are: +- [x] See whether the selected song is already in a playlist +- [x] See whether multiple songs are already in a playlist +- [x] Do not add songs to playlist which would become duplicates +- [x] Material Design for the checkmark +- [x] Remove a song from a playlist without having to manually search in the playlist +- [ ] Option to remove duplicate songs from existing playlists +- [ ] Sync playlists (with other devices, using textfiles export and imports) + +In the playlist-menu checkboxes indicate whether a song / a number of songs are already in a list. +If multiple songs were selected and only some of them are in the playlist the checkbox will not be checked. Instead a checkmark in brackets will be shown at the end of the playlist name. +This is not a good design but very functional. The plan is to implement a third state of the checkbox which will replace this behaviour. + +This fork is different from kabouzeid/master in these and more commits: + +[6bac337](https://github.com/Sogolumbo/Phonograph/commit/6bac3379636d97a68f50ebb1672654ef1aa310fb), +[d91f11a](https://github.com/Sogolumbo/Phonograph/commit/d91f11ad068192806979da79a0d089835d574524), +[e81f655](https://github.com/Sogolumbo/Phonograph/commit/e81f655c802bb2953d6e6d093cc3a1c774b897c4), +[8254527](https://github.com/Sogolumbo/Phonograph/commit/8254527339ba7e8acd5cc522b34e3ee724ba9b5a), +[4512b43](https://github.com/Sogolumbo/Phonograph/commit/4512b43529231a57636b7d62fcbade9fb81329b9), +[c477a6d](https://github.com/Sogolumbo/Phonograph/commit/c477a6db7713f73f4252040eb4990b3ff97d9595). + +## Contributing +I love any support, feedback or new ideas. So feel free to contribute in any possible way. I don't have any experience with android programming so I really need help D: -[Get it on F-Droid](https://f-droid.org/packages/com.kabouzeid.gramophone/) -[Get it on Google Play](https://play.google.com/store/apps/details?id=com.kabouzeid.gramophone) +## Download +Binaries are available under [releases](https://github.com/Sogolumbo/Phonograph/releases). diff --git a/app/src/main/java/com/kabouzeid/gramophone/dialogs/AddToPlaylistDialog.java b/app/src/main/java/com/kabouzeid/gramophone/dialogs/AddToPlaylistDialog.java index a500fe68e..0917c7b7c 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/dialogs/AddToPlaylistDialog.java +++ b/app/src/main/java/com/kabouzeid/gramophone/dialogs/AddToPlaylistDialog.java @@ -1,15 +1,18 @@ package com.kabouzeid.gramophone.dialogs; import android.app.Dialog; +import android.content.DialogInterface; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; +import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.kabouzeid.gramophone.R; import com.kabouzeid.gramophone.loader.PlaylistLoader; import com.kabouzeid.gramophone.model.Playlist; import com.kabouzeid.gramophone.model.Song; +import com.kabouzeid.gramophone.util.MusicUtil; import com.kabouzeid.gramophone.util.PlaylistsUtil; import java.util.ArrayList; @@ -40,26 +43,84 @@ public static AddToPlaylistDialog create(List songs) { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final List playlists = PlaylistLoader.getAllPlaylists(getActivity()); - CharSequence[] playlistNames = new CharSequence[playlists.size() + 1]; - playlistNames[0] = getActivity().getResources().getString(R.string.action_new_playlist); - for (int i = 1; i < playlistNames.length; i++) { - playlistNames[i] = playlists.get(i - 1).name; + CharSequence[] playlistNames = new CharSequence[playlists.size()]; + for (int i = 0; i < playlistNames.length; i++) { + playlistNames[i] = playlists.get(i).name; } + final ArrayList songs = getArguments().getParcelableArrayList("songs"); + + int[] songIds = new int[songs.size()]; + + boolean isAnySongInPlaylist[] = new boolean[playlists.size()]; + boolean areAllSongsInPlaylist[] = new boolean[playlists.size()]; + List checkedPlaylists = new ArrayList(); + + if(songs != null) + { + for(int i = 0; i < songs.size(); i++){ + songIds[i] = songs.get(i).id; + } + + for (int i = 0; i < playlists.size(); i++) { + int playlistId = playlists.get(i).id; + + isAnySongInPlaylist[i] = PlaylistsUtil.doPlaylistContainsAnySong(getActivity(), playlistId, songIds); + areAllSongsInPlaylist[i] = PlaylistsUtil.doPlaylistContainsAllSongs(getActivity(), playlistId, songIds); + + //TODO: display checkboxes instead of checkmark + if (isAnySongInPlaylist[i]) { + if(areAllSongsInPlaylist[i]){ + playlistNames[i] = playlists.get(i).name + " \u2713"; //Add checkmark + } + else{ + playlistNames[i] = playlists.get(i).name + " (\u2713)"; //Add checkmark in brackets + } + } + + if (areAllSongsInPlaylist[i]){ + checkedPlaylists.add(i); + } + } + } + + Integer[] temp = checkedPlaylists.toArray(new Integer[0]); //TODO + + + return new MaterialDialog.Builder(getActivity()) .title(R.string.add_playlist_title) .items(playlistNames) - .itemsCallback((materialDialog, view, i, charSequence) -> { - //noinspection unchecked - final List songs = getArguments().getParcelableArrayList("songs"); - if (songs == null) return; - if (i == 0) { - materialDialog.dismiss(); + .itemsCallbackMultiChoice(temp, new MaterialDialog.ListCallbackMultiChoice() { //TODO + @Override + public boolean onSelection(MaterialDialog materialDialog, Integer[] which, CharSequence[] charSequence) { + boolean[] checked = new boolean[playlistNames.length]; + for (int i: which){ + checked[i] = true; + } + for (int i = 0; i < playlists.size(); i++){ + if (checked[i] ^ areAllSongsInPlaylist[i]){ + if(checked[i]){ + PlaylistsUtil.addToPlaylistWithoutDuplicates(getActivity(), songs, songIds, playlists.get(i).id, true); + } + else{ + for(Song song : songs){ + PlaylistsUtil.removeFromPlaylist(getActivity(), song, playlists.get(i).id); + } + } + } + } + return true; + } + }) + .positiveText(R.string.action_ok) + .neutralText(R.string.action_new_playlist) + .onNeutral( new MaterialDialog.SingleButtonCallback(){ + @Override + public void onClick(MaterialDialog dialog, DialogAction action){ CreatePlaylistDialog.create(songs).show(getActivity().getSupportFragmentManager(), "ADD_TO_PLAYLIST"); - } else { - materialDialog.dismiss(); - PlaylistsUtil.addToPlaylist(getActivity(), songs, playlists.get(i - 1).id, true); } }) .build(); + } } diff --git a/app/src/main/java/com/kabouzeid/gramophone/util/PlaylistsUtil.java b/app/src/main/java/com/kabouzeid/gramophone/util/PlaylistsUtil.java index cec8671cd..5f81743e1 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/util/PlaylistsUtil.java +++ b/app/src/main/java/com/kabouzeid/gramophone/util/PlaylistsUtil.java @@ -13,7 +13,9 @@ import android.widget.Toast; import com.kabouzeid.gramophone.R; +import com.kabouzeid.gramophone.adapter.PlaylistAdapter; import com.kabouzeid.gramophone.helper.M3UWriter; +import com.kabouzeid.gramophone.helper.menu.PlaylistMenuHelper; import com.kabouzeid.gramophone.model.Playlist; import com.kabouzeid.gramophone.model.PlaylistSong; import com.kabouzeid.gramophone.model.Song; @@ -21,6 +23,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; @@ -104,7 +107,6 @@ public static void addToPlaylist(@NonNull final Context context, final Song song helperList.add(song); addToPlaylist(context, helperList, playlistId, showToastOnFinish); } - public static void addToPlaylist(@NonNull final Context context, @NonNull final List songs, final int playlistId, final boolean showToastOnFinish) { final int size = songs.size(); final ContentResolver resolver = context.getContentResolver(); @@ -140,6 +142,20 @@ public static void addToPlaylist(@NonNull final Context context, @NonNull final } } + public static void addToPlaylistWithoutDuplicates(@NonNull final Context context, @NonNull final List songs, final int[] songIds, final int playlistId, final boolean showToastOnFinish){ + boolean[] isSongInPlaylist = doPlaylistContains(context, playlistId, songIds); + addToPlaylistWithoutDuplicates(context, songs, playlistId, isSongInPlaylist, showToastOnFinish); + } + public static void addToPlaylistWithoutDuplicates(@NonNull final Context context, @NonNull final List songs, final int playlistId, final boolean[] isSongInPlaylist,final boolean showToastOnFinish){ + ArrayList helperSongs = new ArrayList(); + for(int i = 0; i < songs.size(); i++){ + if(!isSongInPlaylist[i]){ + helperSongs.add(songs.get(i)); + } + } + addToPlaylist(context, helperSongs, playlistId, showToastOnFinish); + } + @NonNull public static ContentValues[] makeInsertItems(@NonNull final List songs, final int offset, int len, final int base) { if (offset + len > songs.size()) { @@ -204,6 +220,258 @@ public static boolean doPlaylistContains(@NonNull final Context context, final l } return false; } + public static boolean[] doPlaylistContains(@NonNull final Context context, final long playlistId, final int[] songIds) { + if (playlistId != -1) { + final int[] songIdsOriginalOrder = songIds; + int[] songIdsSorted = songIds.clone(); + java.util.Arrays.sort(songIdsSorted); + + boolean[] PlaylistContainsSongSorted = new boolean[songIdsSorted.length]; + boolean[] PlaylistContainsSongOriginalOrder; + + try { + Cursor playlistSongs = context.getContentResolver().query( + MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId), + new String[]{MediaStore.Audio.Playlists.Members.AUDIO_ID}, null, new String[]{}, MediaStore.Audio.Playlists.Members.AUDIO_ID + " ASC"); + + if (playlistSongs != null && playlistSongs.getCount() > 0) { + playlistSongs.moveToNext(); //goes to first element + + int songIndex = 0; + int playlistIndex = 0; + while (true) + { + if(songIndex >= songIdsSorted.length){ + break; + } + int playlistSong = playlistSongs.getInt(0); + if(songIdsSorted[songIndex] > playlistSong) + { + playlistIndex++; + if(playlistIndex < playlistSongs.getCount()) + { + playlistSongs.moveToNext(); + } + else + { + break; + } + } + else if (songIdsSorted[songIndex] < playlistSong) + { + PlaylistContainsSongSorted[songIndex] = false; + songIndex++; + } + else if(songIdsSorted[songIndex] == playlistSong) + { + PlaylistContainsSongSorted[songIndex] = true; + songIndex++; + } + } + playlistSongs.close(); + } + } catch (SecurityException ignored) { + } + //long[] partTimes = new long[songIds.length];//TODO: remove stopwatch + //long startTime = System.currentTimeMillis();//TODO: remove stopwatch + //Revert the sorting to return the right results + PlaylistContainsSongOriginalOrder = PlaylistContainsSongSorted.clone(); + for(int i = 0; i < songIdsSorted.length; i++){ + //partTimes[i] = System.currentTimeMillis();//TODO: remove stopwatch + if(songIdsOriginalOrder[i] == songIdsSorted[i]){ + continue; + } + else{ + int indexOfSongInSortedArray = i; + int compareValue; + + int max = songIds.length; + int min = -1; + while(max - min > 15){ //this is a random number which I hope will result in a good efficiency. Tests showed it should be below 100. + compareValue = Integer.compare(songIdsOriginalOrder[i], songIdsSorted[indexOfSongInSortedArray]); + if(compareValue > 0){ + min = indexOfSongInSortedArray; + } + else{ + max = indexOfSongInSortedArray; + } + indexOfSongInSortedArray = (min + max)/2; + } + indexOfSongInSortedArray = min + 1; + while (songIdsOriginalOrder[i] != songIdsSorted[indexOfSongInSortedArray]){ + indexOfSongInSortedArray++; + } + PlaylistContainsSongOriginalOrder[i] = PlaylistContainsSongSorted[indexOfSongInSortedArray]; + } + } + /* + long stopTimeTotal = System.currentTimeMillis();//TODO: remove stopwatch + long[] diffs = new long[songIds.length];//TODO: remove stopwatch + diffs[songIds.length-1] = stopTimeTotal - partTimes[songIds.length-1]; + long min = diffs[songIds.length-1]; //TODO: remove stopwatch + long max = diffs[songIds.length-1]; //TODO: remove stopwatch + long sum = diffs[songIds.length-1]; //TODO: remove stopwatch + for (int i = 0; i < songIdsSorted.length - 1; i++) { + diffs[i] = partTimes[i+1] - partTimes[i]; + sum += diffs[i]; + min = Math.min(diffs[i], min); + max = Math.max(diffs[i], max); + } + float mean = (float)sum / partTimes.length; + long TotalTime = stopTimeTotal - startTime;//TODO: remove stopwatch + */ + return PlaylistContainsSongOriginalOrder; + } + else{ + throw new IllegalArgumentException("Must be a non-negative integer"); + } + } + public static int doPlaylistContainsCount(@NonNull final Context context, final long playlistId, final int[] songIds) { + return playlistContainsCount(context, doPlaylistContains(context, playlistId, songIds)); + } + public static int playlistContainsCount(@NonNull final Context context, boolean[] isSongInPlaylist) { + int count = 0; + for(boolean isThisSongInPlaylist : isSongInPlaylist){ + if(isThisSongInPlaylist){ + count++; + } + } + return count; + } + public static boolean doPlaylistContainsAnySong(@NonNull final Context context, final long playlistId, final int[] songIds) { + if (playlistId != -1) { + int[] songIdsSorted = songIds.clone(); + java.util.Arrays.sort(songIdsSorted); + + boolean result = false; + + try { + Cursor playlistSongs = context.getContentResolver().query( + MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId), + new String[]{MediaStore.Audio.Playlists.Members.AUDIO_ID}, null, new String[]{}, MediaStore.Audio.Playlists.Members.AUDIO_ID + " ASC"); + + if (playlistSongs != null && playlistSongs.getCount() > 0) { + playlistSongs.moveToNext(); //goes to first element + + int songIndex = 0; + int playlistIndex = 0; + while (true) + { + if(songIndex >= songIdsSorted.length){ + break; + } + int playlistSong = playlistSongs.getInt(0); + if(songIdsSorted[songIndex] > playlistSong) + { + playlistIndex++; + if(playlistIndex < playlistSongs.getCount()) + { + playlistSongs.moveToNext(); + } + else + { + break; + } + } + else if (songIdsSorted[songIndex] < playlistSong) + { + songIndex++; + } + else if(songIdsSorted[songIndex] == playlistSong) + { + result = true; + songIndex++; + break; + } + } + playlistSongs.close(); + } + } catch (SecurityException ignored) { + } + + return result; + } + else{ + throw new IllegalArgumentException("Must be a non-negative integer"); + } + } + public static boolean playlistContainsAnySong(@NonNull final Context context, boolean[] isSongInPlaylist) { + for(boolean isThisSongInPlaylist : isSongInPlaylist){ + if(isThisSongInPlaylist){ + return true; + } + } + return false; + } + public static boolean doPlaylistContainsAllSongs(@NonNull final Context context, final long playlistId, final int[] songIds) { + if (playlistId != -1) { + int[] songIdsSorted = songIds.clone(); + java.util.Arrays.sort(songIdsSorted); + + boolean result = true; + + try { + Cursor playlistSongs = context.getContentResolver().query( + MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId), + new String[]{MediaStore.Audio.Playlists.Members.AUDIO_ID}, null, new String[]{}, MediaStore.Audio.Playlists.Members.AUDIO_ID + " ASC"); + + if (playlistSongs != null && playlistSongs.getCount() > 0) { + playlistSongs.moveToNext(); //goes to first element + + int songIndex = 0; + int playlistIndex = 0; + while (true) + { + if(songIndex >= songIdsSorted.length){ + break; //all songs were in the playlist + } + int playlistSong = playlistSongs.getInt(0); + if(songIdsSorted[songIndex] > playlistSong) + { + playlistIndex++; + if(playlistIndex < playlistSongs.getCount()) + { + playlistSongs.moveToNext(); + } + else + { + result = false; + break; //there are songs missing, but the playlist has no songs left. + } + } + else if (songIdsSorted[songIndex] < playlistSong) + { + result = false; + songIndex++; + break; //at least one song was not in the playlist + } + else if(songIdsSorted[songIndex] == playlistSong) + { + songIndex++; + } + } + playlistSongs.close(); + } + else { + result = false; + } + } catch (SecurityException ignored) { + } + + return result; + } + else{ + throw new IllegalArgumentException("Must be a non-negative integer"); + } + } + public static boolean playlistContainsAllSongs(@NonNull final Context context, boolean[] isSongInPlaylist) { + for(boolean isThisSongInPlaylist : isSongInPlaylist){ + if(!isThisSongInPlaylist){ + return false; + } + } + return true; + } public static boolean moveItem(@NonNull final Context context, int playlistId, int from, int to) { return MediaStore.Audio.Playlists.Members.moveItem(context.getContentResolver(), diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml new file mode 100644 index 000000000..1eb1c70cf --- /dev/null +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/layout/item_list_checkbox.xml b/app/src/main/res/layout/item_list_checkbox.xml new file mode 100644 index 000000000..ddfa9947b --- /dev/null +++ b/app/src/main/res/layout/item_list_checkbox.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 59f257dc4..1100330d7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -28,6 +28,7 @@ Löschen Scannen Als Start-Verzeichnis festlegen + Ok Alben Interpreten Genres diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index bdd6daa9a..c919d042a 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -28,6 +28,7 @@ Delete Scan Set as start directory + Ok Albums Artists Genres diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bded8aa70..4a184b679 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ Delete Scan Set as start directory + Ok Albums Artists Genres