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))
+
[](https://github.com/kabouzeid/Phonograph/blob/master/LICENSE.txt)
**A material designed local music player for Android.**
-
+## 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:
-[
](https://f-droid.org/packages/com.kabouzeid.gramophone/)
-[
](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