From c3e475be6f5855fdc518a62fb78eb1dfd0f69c60 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 13 Nov 2020 18:45:55 +0100 Subject: [PATCH] Allow to export/import --- app/src/acad/res/values/strings.xml | 8 + app/src/acad/res/xml/file_paths.xml | 6 + app/src/fdroid_acad/AndroidManifest.xml | 40 ++ app/src/full/res/values/strings.xml | 8 + app/src/full/res/xml/file_paths.xml | 7 + app/src/google_acad/AndroidManifest.xml | 40 ++ app/src/main/AndroidManifest.xml | 11 + .../app/fedilab/fedilabtube/MainActivity.java | 10 +- .../fedilabtube/PlaylistsActivity.java | 19 + .../fedilabtube/ShowChannelActivity.java | 5 +- .../fedilabtube/client/data/VideoData.java | 74 ++++ .../client/data/VideoPlaylistData.java | 90 +++++ .../client/entities/StreamingPlaylists.java | 68 +++- .../client/entities/ViewsPerDay.java | 33 +- .../drawer/AboutInstanceAdapter.java | 2 - .../fedilabtube/drawer/PlaylistAdapter.java | 107 +++++- .../fragment/DisplayVideosFragment.java | 1 - .../helper/PlaylistExportHelper.java | 105 ++++++ .../sqlite/ManagePlaylistsDAO.java | 344 ++++++++++++++++++ .../fedilab/fedilabtube/sqlite/Sqlite.java | 29 +- .../drawable/ic_baseline_import_export_24.xml | 10 + app/src/main/res/menu/playlist_menu.xml | 6 + 22 files changed, 1009 insertions(+), 14 deletions(-) create mode 100644 app/src/acad/res/xml/file_paths.xml create mode 100644 app/src/fdroid_acad/AndroidManifest.xml create mode 100644 app/src/full/res/xml/file_paths.xml create mode 100644 app/src/google_acad/AndroidManifest.xml create mode 100644 app/src/main/java/app/fedilab/fedilabtube/helper/PlaylistExportHelper.java create mode 100644 app/src/main/java/app/fedilab/fedilabtube/sqlite/ManagePlaylistsDAO.java create mode 100644 app/src/main/res/drawable/ic_baseline_import_export_24.xml diff --git a/app/src/acad/res/values/strings.xml b/app/src/acad/res/values/strings.xml index 17ab7b0..c3612d1 100644 --- a/app/src/acad/res/values/strings.xml +++ b/app/src/acad/res/values/strings.xml @@ -18,6 +18,14 @@ Vidéos dans une liste Change la mise en page pour afficher les vidéos dans une liste + Exporter + Importer + + Exportation réussie ! + Cliquer ici pour envoyer l\'exportation par mèl. + Nouvelle liste de lecture + Ouvrez la pièce jointe avec l\'application TubeAcad + Montrer plus Montrer moins Aucune instance ! diff --git a/app/src/acad/res/xml/file_paths.xml b/app/src/acad/res/xml/file_paths.xml new file mode 100644 index 0000000..4175376 --- /dev/null +++ b/app/src/acad/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/fdroid_acad/AndroidManifest.xml b/app/src/fdroid_acad/AndroidManifest.xml new file mode 100644 index 0000000..5b4678e --- /dev/null +++ b/app/src/fdroid_acad/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/full/res/values/strings.xml b/app/src/full/res/values/strings.xml index ab9e427..d34fdfa 100644 --- a/app/src/full/res/values/strings.xml +++ b/app/src/full/res/values/strings.xml @@ -283,6 +283,14 @@ All + + Export + Import + Successful export! + Tap here to send the export by email + New Playlist + Open the attached file with TubeLab + Playlists Display name You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist diff --git a/app/src/full/res/xml/file_paths.xml b/app/src/full/res/xml/file_paths.xml new file mode 100644 index 0000000..2d5ce66 --- /dev/null +++ b/app/src/full/res/xml/file_paths.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/google_acad/AndroidManifest.xml b/app/src/google_acad/AndroidManifest.xml new file mode 100644 index 0000000..5b4678e --- /dev/null +++ b/app/src/google_acad/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad808b1..dbad8e1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -161,6 +161,17 @@ android:authorities="${applicationId}.workmanager-init" tools:node="remove" android:exported="false" /> + + + + + \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/fedilabtube/MainActivity.java b/app/src/main/java/app/fedilab/fedilabtube/MainActivity.java index 73c8462..9c1e99a 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/MainActivity.java +++ b/app/src/main/java/app/fedilab/fedilabtube/MainActivity.java @@ -65,6 +65,7 @@ import app.fedilab.fedilabtube.client.entities.WellKnownNodeinfo; import app.fedilab.fedilabtube.fragment.DisplayOverviewFragment; import app.fedilab.fedilabtube.fragment.DisplayVideosFragment; import app.fedilab.fedilabtube.helper.Helper; +import app.fedilab.fedilabtube.helper.PlaylistExportHelper; import app.fedilab.fedilabtube.helper.SwitchAccountHelper; import app.fedilab.fedilabtube.services.RetrieveInfoService; import app.fedilab.fedilabtube.sqlite.AccountDAO; @@ -84,6 +85,7 @@ public class MainActivity extends AppCompatActivity { public static int PICK_INSTANCE = 5641; public static int PICK_INSTANCE_SURF = 5642; public static UserMe userMe; + public static TypeOfConnection typeOfConnection; final FragmentManager fm = getSupportFragmentManager(); Fragment active; private DisplayVideosFragment recentFragment, locaFragment, trendingFragment, subscriptionFragment, mostLikedFragment; @@ -121,7 +123,6 @@ public class MainActivity extends AppCompatActivity { return false; } }; - public static TypeOfConnection typeOfConnection; @SuppressLint("ApplySharedPref") public static void showRadioButtonDialogFullInstances(Activity activity, boolean storeInDb) { @@ -359,7 +360,9 @@ public class MainActivity extends AppCompatActivity { RateThisApp.onCreate(this); RateThisApp.showRateDialogIfNeeded(this); } - + if (!BuildConfig.full_instances) { + PlaylistExportHelper.manageIntentUrl(MainActivity.this, getIntent()); + } } private void startInForeground() { @@ -611,9 +614,12 @@ public class MainActivity extends AppCompatActivity { if (extras.getInt(Helper.INTENT_ACTION) == Helper.ADD_USER_INTENT) { recreate(); } + } else if (!BuildConfig.full_instances) { + PlaylistExportHelper.manageIntentUrl(MainActivity.this, intent); } } + @SuppressLint("ApplySharedPref") private void showRadioButtonDialog() { diff --git a/app/src/main/java/app/fedilab/fedilabtube/PlaylistsActivity.java b/app/src/main/java/app/fedilab/fedilabtube/PlaylistsActivity.java index 113bb94..5147f41 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/PlaylistsActivity.java +++ b/app/src/main/java/app/fedilab/fedilabtube/PlaylistsActivity.java @@ -14,6 +14,7 @@ package app.fedilab.fedilabtube; * You should have received a copy of the GNU General Public License along with TubeLab; if not, * see . */ +import android.content.Intent; import android.os.Bundle; import android.view.MenuItem; import android.widget.Toast; @@ -24,12 +25,14 @@ import androidx.fragment.app.FragmentTransaction; import app.fedilab.fedilabtube.client.data.PlaylistData; import app.fedilab.fedilabtube.fragment.DisplayVideosFragment; import app.fedilab.fedilabtube.helper.Helper; +import app.fedilab.fedilabtube.helper.PlaylistExportHelper; import app.fedilab.fedilabtube.viewmodel.TimelineVM; import es.dmoral.toasty.Toasty; public class PlaylistsActivity extends AppCompatActivity { + private final int PICK_IMPORT = 5556; @Override protected void onCreate(Bundle savedInstanceState) { @@ -66,6 +69,22 @@ public class PlaylistsActivity extends AppCompatActivity { } + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PICK_IMPORT && resultCode == RESULT_OK) { + if (data == null || data.getData() == null) { + Toasty.error(PlaylistsActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + return; + } + PlaylistExportHelper.manageIntentUrl(PlaylistsActivity.this, data); + + } else if (requestCode == PICK_IMPORT) { + Toasty.error(PlaylistsActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { diff --git a/app/src/main/java/app/fedilab/fedilabtube/ShowChannelActivity.java b/app/src/main/java/app/fedilab/fedilabtube/ShowChannelActivity.java index a01d2c7..faae6f7 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/ShowChannelActivity.java +++ b/app/src/main/java/app/fedilab/fedilabtube/ShowChannelActivity.java @@ -20,8 +20,6 @@ import android.content.res.ColorStateList; import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.text.Html; import android.text.SpannableString; import android.text.method.LinkMovementMethod; @@ -72,7 +70,6 @@ import app.fedilab.fedilabtube.viewmodel.TimelineVM; import es.dmoral.toasty.Toasty; import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY; -import static app.fedilab.fedilabtube.MainActivity.TypeOfConnection.NORMAL; import static app.fedilab.fedilabtube.MainActivity.TypeOfConnection.SURFING; import static app.fedilab.fedilabtube.client.RetrofitPeertubeAPI.ActionType.FOLLOW; import static app.fedilab.fedilabtube.client.RetrofitPeertubeAPI.ActionType.MUTE; @@ -230,7 +227,7 @@ public class ShowChannelActivity extends AppCompatActivity { } catch (Exception e) { Toasty.error(ShowChannelActivity.this, getString(R.string.toast_error), Toasty.LENGTH_LONG).show(); } - }else if (item.getItemId() == R.id.action_display_account) { + } else if (item.getItemId() == R.id.action_display_account) { Bundle b = new Bundle(); Intent intent = new Intent(ShowChannelActivity.this, ShowAccountActivity.class); b.putParcelable("account", channel.getOwnerAccount()); diff --git a/app/src/main/java/app/fedilab/fedilabtube/client/data/VideoData.java b/app/src/main/java/app/fedilab/fedilabtube/client/data/VideoData.java index 3dd0859..2a8676e 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/client/data/VideoData.java +++ b/app/src/main/java/app/fedilab/fedilabtube/client/data/VideoData.java @@ -755,4 +755,78 @@ public class VideoData { this.description = description; } } + + + public static class VideoExport implements Parcelable { + public static final Creator CREATOR = new Creator() { + @Override + public VideoExport createFromParcel(Parcel in) { + return new VideoExport(in); + } + + @Override + public VideoExport[] newArray(int size) { + return new VideoExport[size]; + } + }; + private int id; + private String uuid; + private Video videoData; + private int playlistDBid; + + public VideoExport() { + } + + protected VideoExport(Parcel in) { + id = in.readInt(); + uuid = in.readString(); + videoData = in.readParcelable(Video.class.getClassLoader()); + playlistDBid = in.readInt(); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Video getVideoData() { + return videoData; + } + + public void setVideoData(Video videoData) { + this.videoData = videoData; + } + + public int getPlaylistDBid() { + return playlistDBid; + } + + public void setPlaylistDBid(int playlistDBid) { + this.playlistDBid = playlistDBid; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeInt(id); + parcel.writeString(uuid); + parcel.writeParcelable(videoData, i); + parcel.writeInt(playlistDBid); + } + } } diff --git a/app/src/main/java/app/fedilab/fedilabtube/client/data/VideoPlaylistData.java b/app/src/main/java/app/fedilab/fedilabtube/client/data/VideoPlaylistData.java index 4f21e13..9e2db1f 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/client/data/VideoPlaylistData.java +++ b/app/src/main/java/app/fedilab/fedilabtube/client/data/VideoPlaylistData.java @@ -15,11 +15,15 @@ package app.fedilab.fedilabtube.client.data; * see . */ +import android.os.Parcel; +import android.os.Parcelable; + import com.google.gson.annotations.SerializedName; import java.util.List; + @SuppressWarnings({"unused", "RedundantSuppression"}) public class VideoPlaylistData { @@ -155,4 +159,90 @@ public class VideoPlaylistData { this.uuid = uuid; } } + + public static class VideoPlaylistExport implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public VideoPlaylistExport createFromParcel(Parcel in) { + return new VideoPlaylistExport(in); + } + + @Override + public VideoPlaylistExport[] newArray(int size) { + return new VideoPlaylistExport[size]; + } + }; + private long playlistDBkey; + private String acct; + private String uuid; + private PlaylistData.Playlist playlist; + private List videos; + + + public VideoPlaylistExport() { + } + + protected VideoPlaylistExport(Parcel in) { + playlistDBkey = in.readLong(); + acct = in.readString(); + uuid = in.readString(); + playlist = in.readParcelable(PlaylistData.Playlist.class.getClassLoader()); + in.readList(this.videos, VideoPlaylistData.VideoPlaylist.class.getClassLoader()); + } + + public String getAcct() { + return acct; + } + + public void setAcct(String acct) { + this.acct = acct; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public PlaylistData.Playlist getPlaylist() { + return playlist; + } + + public void setPlaylist(PlaylistData.Playlist playlist) { + this.playlist = playlist; + } + + public long getPlaylistDBkey() { + return playlistDBkey; + } + + public void setPlaylistDBkey(long playlistDBkey) { + this.playlistDBkey = playlistDBkey; + } + + public List getVideos() { + return videos; + } + + public void setVideos(List videos) { + this.videos = videos; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeLong(playlistDBkey); + parcel.writeString(acct); + parcel.writeString(uuid); + parcel.writeParcelable(playlist, i); + parcel.writeList(videos); + } + } } diff --git a/app/src/main/java/app/fedilab/fedilabtube/client/entities/StreamingPlaylists.java b/app/src/main/java/app/fedilab/fedilabtube/client/entities/StreamingPlaylists.java index f3ea3e2..dca4c6d 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/client/entities/StreamingPlaylists.java +++ b/app/src/main/java/app/fedilab/fedilabtube/client/entities/StreamingPlaylists.java @@ -14,13 +14,27 @@ package app.fedilab.fedilabtube.client.entities; * You should have received a copy of the GNU General Public License along with TubeLab; if not, * see . */ +import android.os.Parcel; +import android.os.Parcelable; + import com.google.gson.annotations.SerializedName; import java.util.List; @SuppressWarnings({"unused", "RedundantSuppression"}) -public class StreamingPlaylists { +public class StreamingPlaylists implements Parcelable { + public static final Creator CREATOR = new Creator() { + @Override + public StreamingPlaylists createFromParcel(Parcel in) { + return new StreamingPlaylists(in); + } + + @Override + public StreamingPlaylists[] newArray(int size) { + return new StreamingPlaylists[size]; + } + }; @SerializedName("id") private String id; @SerializedName("type") @@ -34,6 +48,15 @@ public class StreamingPlaylists { @SerializedName("redundancies") private List redundancies; + protected StreamingPlaylists(Parcel in) { + id = in.readString(); + type = in.readInt(); + playlistUrl = in.readString(); + segmentsSha256Url = in.readString(); + files = in.createTypedArrayList(File.CREATOR); + redundancies = in.createTypedArrayList(Redundancies.CREATOR); + } + public String getId() { return id; } @@ -82,10 +105,41 @@ public class StreamingPlaylists { this.redundancies = redundancies; } - public static class Redundancies { + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(id); + parcel.writeInt(type); + parcel.writeString(playlistUrl); + parcel.writeString(segmentsSha256Url); + parcel.writeTypedList(files); + parcel.writeTypedList(redundancies); + } + + + public static class Redundancies implements Parcelable { + public static final Creator CREATOR = new Creator() { + @Override + public Redundancies createFromParcel(Parcel in) { + return new Redundancies(in); + } + + @Override + public Redundancies[] newArray(int size) { + return new Redundancies[size]; + } + }; @SerializedName("baseUrl") private String baseUrl; + protected Redundancies(Parcel in) { + baseUrl = in.readString(); + } + public String getBaseUrl() { return baseUrl; } @@ -93,5 +147,15 @@ public class StreamingPlaylists { public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(baseUrl); + } } } diff --git a/app/src/main/java/app/fedilab/fedilabtube/client/entities/ViewsPerDay.java b/app/src/main/java/app/fedilab/fedilabtube/client/entities/ViewsPerDay.java index d1c372c..cba2d80 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/client/entities/ViewsPerDay.java +++ b/app/src/main/java/app/fedilab/fedilabtube/client/entities/ViewsPerDay.java @@ -14,18 +14,38 @@ package app.fedilab.fedilabtube.client.entities; * You should have received a copy of the GNU General Public License along with TubeLab; if not, * see . */ +import android.os.Parcel; +import android.os.Parcelable; + import com.google.gson.annotations.SerializedName; import java.util.Date; @SuppressWarnings({"unused", "RedundantSuppression"}) -public class ViewsPerDay { +public class ViewsPerDay implements Parcelable { + public static final Creator CREATOR = new Creator() { + @Override + public ViewsPerDay createFromParcel(Parcel in) { + return new ViewsPerDay(in); + } + + @Override + public ViewsPerDay[] newArray(int size) { + return new ViewsPerDay[size]; + } + }; @SerializedName("date") private Date date; @SerializedName("views") private int views; + protected ViewsPerDay(Parcel in) { + long tmpDate = in.readLong(); + this.date = tmpDate == -1 ? null : new Date(tmpDate); + views = in.readInt(); + } + public Date getDate() { return date; } @@ -41,4 +61,15 @@ public class ViewsPerDay { public void setViews(int views) { this.views = views; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeLong(this.date != null ? this.date.getTime() : -1); + parcel.writeInt(views); + } } diff --git a/app/src/main/java/app/fedilab/fedilabtube/drawer/AboutInstanceAdapter.java b/app/src/main/java/app/fedilab/fedilabtube/drawer/AboutInstanceAdapter.java index c6f5acf..b918bc1 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/drawer/AboutInstanceAdapter.java +++ b/app/src/main/java/app/fedilab/fedilabtube/drawer/AboutInstanceAdapter.java @@ -19,7 +19,6 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.os.Build; @@ -37,7 +36,6 @@ import androidx.recyclerview.widget.RecyclerView; import java.util.List; -import app.fedilab.fedilabtube.MainActivity; import app.fedilab.fedilabtube.R; import app.fedilab.fedilabtube.client.data.InstanceData; import app.fedilab.fedilabtube.databinding.DrawerAboutInstanceBinding; diff --git a/app/src/main/java/app/fedilab/fedilabtube/drawer/PlaylistAdapter.java b/app/src/main/java/app/fedilab/fedilabtube/drawer/PlaylistAdapter.java index a1aa6c9..d697dd2 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/drawer/PlaylistAdapter.java +++ b/app/src/main/java/app/fedilab/fedilabtube/drawer/PlaylistAdapter.java @@ -14,10 +14,19 @@ package app.fedilab.fedilabtube.drawer; * You should have received a copy of the GNU General Public License along with TubeLab; if not, * see . */ +import android.Manifest; +import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -29,19 +38,39 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.appcompat.widget.PopupMenu; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.FutureTarget; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.util.List; +import java.util.concurrent.ExecutionException; import app.fedilab.fedilabtube.AllPlaylistsActivity; +import app.fedilab.fedilabtube.BuildConfig; +import app.fedilab.fedilabtube.MainActivity; import app.fedilab.fedilabtube.PlaylistsActivity; import app.fedilab.fedilabtube.R; import app.fedilab.fedilabtube.client.APIResponse; +import app.fedilab.fedilabtube.client.RetrofitPeertubeAPI; import app.fedilab.fedilabtube.client.data.PlaylistData.Playlist; +import app.fedilab.fedilabtube.client.data.VideoData; +import app.fedilab.fedilabtube.client.data.VideoPlaylistData; import app.fedilab.fedilabtube.helper.Helper; +import app.fedilab.fedilabtube.helper.NotificationHelper; +import app.fedilab.fedilabtube.helper.PlaylistExportHelper; import app.fedilab.fedilabtube.viewmodel.PlaylistsVM; +import es.dmoral.toasty.Toasty; + +import static app.fedilab.fedilabtube.viewmodel.PlaylistsVM.action.GET_LIST_VIDEOS; public class PlaylistAdapter extends BaseAdapter { @@ -122,6 +151,9 @@ public class PlaylistAdapter extends BaseAdapter { PopupMenu popup = new PopupMenu(context, holder.playlist_more); popup.getMenuInflater() .inflate(R.menu.playlist_menu, popup.getMenu()); + if (!BuildConfig.full_instances) { + popup.getMenu().findItem(R.id.action_export).setVisible(true); + } popup.setOnMenuItemClickListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.action_delete) { @@ -145,6 +177,16 @@ public class PlaylistAdapter extends BaseAdapter { if (context instanceof AllPlaylistsActivity) { ((AllPlaylistsActivity) context).manageAlert(playlist); } + } else if (itemId == R.id.action_export) { + if (Build.VERSION.SDK_INT >= 23) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE); + } else { + doExport(playlist); + } + } else { + doExport(playlist); + } } return true; }); @@ -159,6 +201,70 @@ public class PlaylistAdapter extends BaseAdapter { } + private void doExport(Playlist playlist) { + new Thread(() -> { + File file = null; + RetrofitPeertubeAPI retrofitPeertubeAPI = new RetrofitPeertubeAPI(context); + APIResponse apiResponse = retrofitPeertubeAPI.playlistAction(GET_LIST_VIDEOS, playlist.getId(), null, null, null); + if (apiResponse != null) { + List videos = apiResponse.getVideoPlaylist(); + VideoPlaylistData.VideoPlaylistExport videoPlaylistExport = new VideoPlaylistData.VideoPlaylistExport(); + videoPlaylistExport.setPlaylist(playlist); + videoPlaylistExport.setUuid(playlist.getUuid()); + videoPlaylistExport.setAcct(MainActivity.userMe.getAccount().getAcct()); + videoPlaylistExport.setVideos(videos); + + String data = PlaylistExportHelper.playlistToStringStorage(videoPlaylistExport); + + + File root = new File(Environment.getExternalStorageDirectory(), context.getString(R.string.app_name)); + if (!root.exists()) { + root.mkdirs(); + } + file = new File(root, "playlist_" + playlist.getUuid() + ".tubelab"); + FileWriter writer = null; + try { + writer = new FileWriter(file); + writer.append(data); + writer.flush(); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> Toasty.error(context, context.getString(R.string.toast_error), Toasty.LENGTH_LONG).show(); + mainHandler.post(myRunnable); + return; + } + + + } + String urlAvatar = playlist.getThumbnailPath() != null ? Helper.getLiveInstance(context) + playlist.getThumbnailPath() : null; + FutureTarget futureBitmapChannel = Glide.with(context.getApplicationContext()) + .asBitmap() + .load(urlAvatar != null ? urlAvatar : R.drawable.missing_peertube).submit(); + Bitmap icon = null; + try { + icon = futureBitmapChannel.get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + if (file != null) { + Intent mailIntent = new Intent(Intent.ACTION_SEND); + mailIntent.setType("message/rfc822"); + Uri contentUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".fileProvider", file); + mailIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.export_notification_subjet)); + mailIntent.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.export_notification_body)); + mailIntent.putExtra(Intent.EXTRA_STREAM, contentUri); + NotificationHelper.notify_user(context.getApplicationContext(), + playlist.getOwnerAccount(), mailIntent, icon, + context.getString(R.string.export_notification_title), + context.getString(R.string.export_notification_content)); + } + + + }).start(); + } + private static class ViewHolder { LinearLayout playlist_container; ImageView preview_playlist; @@ -166,5 +272,4 @@ public class PlaylistAdapter extends BaseAdapter { ImageButton playlist_more; } - } \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/fedilabtube/fragment/DisplayVideosFragment.java b/app/src/main/java/app/fedilab/fedilabtube/fragment/DisplayVideosFragment.java index a46a950..dde9739 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/fragment/DisplayVideosFragment.java +++ b/app/src/main/java/app/fedilab/fedilabtube/fragment/DisplayVideosFragment.java @@ -446,7 +446,6 @@ public class DisplayVideosFragment extends Fragment implements AccountsHorizonta } - public void scrollToTop() { if (mLayoutManager != null) { mLayoutManager.scrollToPositionWithOffset(0, 0); diff --git a/app/src/main/java/app/fedilab/fedilabtube/helper/PlaylistExportHelper.java b/app/src/main/java/app/fedilab/fedilabtube/helper/PlaylistExportHelper.java new file mode 100644 index 0000000..d57b3d7 --- /dev/null +++ b/app/src/main/java/app/fedilab/fedilabtube/helper/PlaylistExportHelper.java @@ -0,0 +1,105 @@ +package app.fedilab.fedilabtube.helper; +/* Copyright 2020 Thomas Schneider + * + * This file is a part of TubeLab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * TubeLab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with TubeLab; if not, + * see . */ + +import android.app.Activity; +import android.content.Intent; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +import app.fedilab.fedilabtube.PlaylistsActivity; +import app.fedilab.fedilabtube.client.data.VideoPlaylistData; +import app.fedilab.fedilabtube.sqlite.ManagePlaylistsDAO; +import app.fedilab.fedilabtube.sqlite.Sqlite; + +public class PlaylistExportHelper { + + + /** + * Unserialized VideoPlaylistExport + * + * @param serializedVideoPlaylistExport String serialized VideoPlaylistExport + * @return VideoPlaylistExport + */ + public static VideoPlaylistData.VideoPlaylistExport restorePlaylistFromString(String serializedVideoPlaylistExport) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedVideoPlaylistExport, VideoPlaylistData.VideoPlaylistExport.class); + } catch (Exception e) { + return null; + } + } + + /** + * Serialized VideoPlaylistExport class + * + * @param videoPlaylistExport Playlist to serialize + * @return String serialized VideoPlaylistData.VideoPlaylistExport + */ + public static String playlistToStringStorage(VideoPlaylistData.VideoPlaylistExport videoPlaylistExport) { + Gson gson = new Gson(); + try { + return gson.toJson(videoPlaylistExport); + } catch (Exception e) { + return null; + } + } + + /** + * Manage intent for opening a tubelab file allowing to import a whole playlist and store it in db + * + * @param activity Activity + * @param intent Intent + */ + public static void manageIntentUrl(Activity activity, Intent intent) { + if (intent.getData() != null) { + String url = intent.getData().toString(); + if (url.endsWith(".json")) { + File file = new File(url); + StringBuilder text = new StringBuilder(); + try { + BufferedReader br = new BufferedReader(new FileReader(file)); + String line; + while ((line = br.readLine()) != null) { + text.append(line); + text.append('\n'); + } + br.close(); + } catch (IOException e) { + e.printStackTrace(); + } + if (text.length() > 20) { + new Thread(() -> { + VideoPlaylistData.VideoPlaylistExport videoPlaylistExport = PlaylistExportHelper.restorePlaylistFromString(text.toString()); + if (videoPlaylistExport != null) { + SQLiteDatabase db = Sqlite.getInstance(activity.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + new ManagePlaylistsDAO(activity, db).insertPlaylist(videoPlaylistExport); + } + activity.runOnUiThread(() -> { + Intent intentPlaylist = new Intent(activity, PlaylistsActivity.class); + activity.startActivity(intentPlaylist); + }); + }).start(); + } + } + } + } +} diff --git a/app/src/main/java/app/fedilab/fedilabtube/sqlite/ManagePlaylistsDAO.java b/app/src/main/java/app/fedilab/fedilabtube/sqlite/ManagePlaylistsDAO.java new file mode 100644 index 0000000..b5d99b4 --- /dev/null +++ b/app/src/main/java/app/fedilab/fedilabtube/sqlite/ManagePlaylistsDAO.java @@ -0,0 +1,344 @@ +package app.fedilab.fedilabtube.sqlite; +/* Copyright 2020 Thomas Schneider + * + * This file is a part of TubeLab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * TubeLab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with TubeLab; if not, + * see . */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.fedilabtube.client.data.PlaylistData; +import app.fedilab.fedilabtube.client.data.VideoData; +import app.fedilab.fedilabtube.client.data.VideoPlaylistData; + + +@SuppressWarnings("UnusedReturnValue") +public class ManagePlaylistsDAO { + + private final SQLiteDatabase db; + public Context context; + + + public ManagePlaylistsDAO(Context context, SQLiteDatabase db) { + //Creation of the DB with tables + this.context = context; + this.db = db; + } + + /** + * Unserialized Video + * + * @param serializedVideo String serialized Video + * @return Video + */ + public static VideoData.Video restoreVideoFromString(String serializedVideo) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedVideo, VideoData.Video.class); + } catch (Exception e) { + return null; + } + } + + /** + * Serialized Video class + * + * @param video Video to serialize + * @return String serialized video + */ + public static String videoToStringStorage(VideoData.Video video) { + Gson gson = new Gson(); + try { + return gson.toJson(video); + } catch (Exception e) { + return null; + } + } + + + /** + * Unserialized Playlist + * + * @param serializedPlaylist String serialized Playlist + * @return Playlist + */ + public static PlaylistData.Playlist restorePlaylistFromString(String serializedPlaylist) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedPlaylist, PlaylistData.Playlist.class); + } catch (Exception e) { + return null; + } + } + + /** + * Serialized Playlist class + * + * @param playlist Playlist to serialize + * @return String serialized playlist + */ + public static String playlistToStringStorage(PlaylistData.Playlist playlist) { + Gson gson = new Gson(); + try { + return gson.toJson(playlist); + } catch (Exception e) { + return null; + } + } + + /** + * Insert playlist info in database + * + * @param videoPlaylistExport VideoPlaylistExport + * @return boolean + */ + public boolean insertPlaylist(VideoPlaylistData.VideoPlaylistExport videoPlaylistExport) { + + if (videoPlaylistExport.getPlaylist() == null || checkExists(videoPlaylistExport.getPlaylist().getUuid())) { + return true; + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_ACCT, videoPlaylistExport.getAcct()); + values.put(Sqlite.COL_UUID, videoPlaylistExport.getUuid()); + values.put(Sqlite.COL_PLAYLIST, playlistToStringStorage(videoPlaylistExport.getPlaylist())); + //Inserts playlist + try { + long playlist_id = db.insertOrThrow(Sqlite.TABLE_LOCAL_PLAYLISTS, null, values); + videoPlaylistExport.setPlaylistDBkey(playlist_id); + for (VideoPlaylistData.VideoPlaylist videoPlaylist : videoPlaylistExport.getVideos()) { + //Insert videos + insertVideos(videoPlaylist.getVideo(), videoPlaylistExport); + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + return true; + } + + /** + * Insert videos for playlists in database + * + * @param video Video to insert + * @param playlist VideoPlaylistExport targeted + * @return boolean + */ + private boolean insertVideos(VideoData.Video video, VideoPlaylistData.VideoPlaylistExport playlist) { + + if (video == null || playlist == null || checkVideoExists(video.getUuid(), playlist.getUuid())) { + return true; + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_UUID, video.getUuid()); + values.put(Sqlite.COL_PLAYLIST_ID, playlist.getPlaylistDBkey()); + values.put(Sqlite.COL_VIDEO_DATA, videoToStringStorage(video)); + //Inserts playlist + try { + db.insertOrThrow(Sqlite.TABLE_VIDEOS, null, values); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } + + /** + * Check if playlist exists + * + * @param uuid String + * @return int + */ + private boolean checkExists(String uuid) { + try { + Cursor c = db.query(Sqlite.TABLE_LOCAL_PLAYLISTS, null, Sqlite.COL_UUID + " = \"" + uuid + "\"", null, null, null, null, "1"); + int count = c.getCount(); + c.close(); + return count > 0; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + } + + /** + * Check if playlist exists + * + * @param videoUuid String + * @param playlistUuid String + * @return int + */ + private boolean checkVideoExists(String videoUuid, String playlistUuid) { + try { + String check_query = "SELECT * FROM " + Sqlite.TABLE_LOCAL_PLAYLISTS + " p INNER JOIN " + + Sqlite.TABLE_VIDEOS + " v ON p.id = v." + Sqlite.COL_PLAYLIST_ID + + " WHERE p." + Sqlite.COL_UUID + "=? AND v." + Sqlite.COL_UUID + "=? LIMIT 1"; + Cursor c = db.rawQuery(check_query, new String[]{playlistUuid, videoUuid}); + int count = c.getCount(); + c.close(); + return count > 0; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + } + + + /** + * Remove a playlist with its uuid + * + * @param uuid String uuid of the Playlist + * @return int + */ + public int removePlaylist(String uuid) { + VideoPlaylistData.VideoPlaylistExport videoPlaylistExport = getSinglePlaylists(uuid); + db.delete(Sqlite.TABLE_VIDEOS, Sqlite.COL_PLAYLIST_ID + " = '" + videoPlaylistExport.getPlaylistDBkey() + "'", null); + return db.delete(Sqlite.TABLE_LOCAL_PLAYLISTS, Sqlite.COL_ID + " = '" + videoPlaylistExport.getPlaylistDBkey() + "'", null); + } + + + /** + * Returns a playlist from it's uid in db + * + * @return VideoPlaylistExport + */ + public VideoPlaylistData.VideoPlaylistExport getSinglePlaylists(String uuid) { + + try { + Cursor c = db.query(Sqlite.TABLE_LOCAL_PLAYLISTS, null, Sqlite.COL_UUID + "='" + uuid + "'", null, null, null, Sqlite.COL_ID + " DESC", null); + return cursorToSingleVideoPlaylistExport(c); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + + /** + * Returns all playlists in db + * + * @return List + */ + public List getAllPlaylists() { + + try { + Cursor c = db.query(Sqlite.TABLE_LOCAL_PLAYLISTS, null, null, null, null, null, Sqlite.COL_ID + " DESC", null); + return cursorToVideoPlaylistExport(c); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + + /** + * Returns all videos in a playlist + * + * @return List + */ + public List getAllVideosInPlaylist(VideoPlaylistData.VideoPlaylistExport videoPlaylistExport) { + + try { + Cursor c = db.query(Sqlite.TABLE_VIDEOS, null, Sqlite.COL_PLAYLIST_ID + "='" + videoPlaylistExport.getPlaylistDBkey() + "'", null, null, null, Sqlite.COL_ID + " DESC", null); + return cursorToVideoExport(c); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + + /*** + * Method to hydrate VideoPlaylistExport from database + * @param c Cursor + * @return VideoPlaylistData.VideoPlaylistExport + */ + private VideoPlaylistData.VideoPlaylistExport cursorToSingleVideoPlaylistExport(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + c.moveToFirst(); + VideoPlaylistData.VideoPlaylistExport videoPlaylistExport = new VideoPlaylistData.VideoPlaylistExport(); + videoPlaylistExport.setAcct(c.getString(c.getColumnIndex(Sqlite.COL_ACCT))); + videoPlaylistExport.setUuid(c.getString(c.getColumnIndex(Sqlite.COL_UUID))); + videoPlaylistExport.setPlaylistDBkey(c.getInt(c.getColumnIndex(Sqlite.COL_ID))); + videoPlaylistExport.setPlaylist(restorePlaylistFromString(c.getString(c.getColumnIndex(Sqlite.COL_PLAYLIST)))); + //Close the cursor + c.close(); + return videoPlaylistExport; + } + + + /*** + * Method to hydrate VideoPlaylistExport from database + * @param c Cursor + * @return List + */ + private List cursorToVideoPlaylistExport(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + List videoPlaylistExports = new ArrayList<>(); + while (c.moveToNext()) { + VideoPlaylistData.VideoPlaylistExport videoPlaylistExport = new VideoPlaylistData.VideoPlaylistExport(); + videoPlaylistExport.setAcct(c.getString(c.getColumnIndex(Sqlite.COL_ACCT))); + videoPlaylistExport.setUuid(c.getString(c.getColumnIndex(Sqlite.COL_UUID))); + videoPlaylistExport.setPlaylistDBkey(c.getInt(c.getColumnIndex(Sqlite.COL_ID))); + videoPlaylistExport.setPlaylist(restorePlaylistFromString(c.getString(c.getColumnIndex(Sqlite.COL_PLAYLIST)))); + videoPlaylistExports.add(videoPlaylistExport); + } + //Close the cursor + c.close(); + return videoPlaylistExports; + } + + + /*** + * Method to hydrate Video from database + * @param c Cursor + * @return List + */ + private List cursorToVideoExport(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + List videoExports = new ArrayList<>(); + while (c.moveToNext()) { + VideoData.VideoExport videoExport = new VideoData.VideoExport(); + videoExport.setPlaylistDBid(c.getInt(c.getColumnIndex(Sqlite.COL_PLAYLIST_ID))); + videoExport.setUuid(c.getString(c.getColumnIndex(Sqlite.COL_UUID))); + videoExport.setId(c.getInt(c.getColumnIndex(Sqlite.COL_ID))); + videoExport.setVideoData(restoreVideoFromString(c.getString(c.getColumnIndex(Sqlite.COL_VIDEO_DATA)))); + videoExports.add(videoExport); + } + //Close the cursor + c.close(); + return videoExports; + } + +} diff --git a/app/src/main/java/app/fedilab/fedilabtube/sqlite/Sqlite.java b/app/src/main/java/app/fedilab/fedilabtube/sqlite/Sqlite.java index 6f41ca6..5503449 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/sqlite/Sqlite.java +++ b/app/src/main/java/app/fedilab/fedilabtube/sqlite/Sqlite.java @@ -21,7 +21,7 @@ import android.database.sqlite.SQLiteOpenHelper; public class Sqlite extends SQLiteOpenHelper { - public static final int DB_VERSION = 2; + public static final int DB_VERSION = 3; public static final String DB_NAME = "mastodon_etalab_db"; /*** * List of tables to manage users and data @@ -62,6 +62,11 @@ public class Sqlite extends SQLiteOpenHelper { static final String COL_USER_INSTANCE = "USER_INSTANCE"; static final String TABLE_BOOKMARKED_INSTANCES = "BOOKMARKED_INSTANCES"; static final String COL_ABOUT = "ABOUT"; + static final String TABLE_LOCAL_PLAYLISTS = "LOCAL_PLAYLISTS"; + static final String COL_PLAYLIST = "PLAYLIST"; + static final String TABLE_VIDEOS = "VIDEOS"; + static final String COL_VIDEO_DATA = "VIDEO_DATA"; + static final String COL_PLAYLIST_ID = "PLAYLIST_ID"; private static final String CREATE_TABLE_USER_ACCOUNT = "CREATE TABLE " + TABLE_USER_ACCOUNT + " (" + COL_USER_ID + " TEXT, " + COL_USERNAME + " TEXT NOT NULL, " + COL_ACCT + " TEXT NOT NULL, " + COL_DISPLAYED_NAME + " TEXT NOT NULL, " + COL_LOCKED + " INTEGER NOT NULL, " @@ -92,6 +97,19 @@ public class Sqlite extends SQLiteOpenHelper { + COL_USER_ID + " TEXT NOT NULL, " + COL_ABOUT + " TEXT NOT NULL, " + COL_USER_INSTANCE + " TEXT NOT NULL)"; + private final String CREATE_TABLE_LOCAL_PLAYLISTS = "CREATE TABLE " + + TABLE_LOCAL_PLAYLISTS + "(" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_ACCT + " TEXT NOT NULL, " + + COL_UUID + " TEXT NOT NULL, " + + COL_PLAYLIST + " TEXT NOT NULL)"; + private final String CREATE_TABLE_VIDEOS = "CREATE TABLE " + + TABLE_VIDEOS + "(" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_UUID + " TEXT NOT NULL, " + + COL_VIDEO_DATA + " TEXT NOT NULL, " + + COL_PLAYLIST_ID + " INTEGER, " + + " FOREIGN KEY (" + COL_PLAYLIST_ID + ") REFERENCES " + COL_PLAYLIST + "(" + COL_ID + "));"; public Sqlite(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); @@ -111,13 +129,19 @@ public class Sqlite extends SQLiteOpenHelper { db.execSQL(CREATE_TABLE_USER_ACCOUNT); db.execSQL(CREATE_TABLE_PEERTUBE_FAVOURITES); db.execSQL(CREATE_TABLE_STORED_INSTANCES); + db.execSQL(CREATE_TABLE_LOCAL_PLAYLISTS); + db.execSQL(CREATE_TABLE_VIDEOS); } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { switch (oldVersion) { + case 3: + db.execSQL("DROP TABLE IF EXISTS " + TABLE_VIDEOS); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_LOCAL_PLAYLISTS); case 2: db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKED_INSTANCES); + } } @@ -126,6 +150,9 @@ public class Sqlite extends SQLiteOpenHelper { switch (oldVersion) { case 1: db.execSQL(CREATE_TABLE_STORED_INSTANCES); + case 2: + db.execSQL(CREATE_TABLE_LOCAL_PLAYLISTS); + db.execSQL(CREATE_TABLE_VIDEOS); } } diff --git a/app/src/main/res/drawable/ic_baseline_import_export_24.xml b/app/src/main/res/drawable/ic_baseline_import_export_24.xml new file mode 100644 index 0000000..2418755 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_import_export_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/menu/playlist_menu.xml b/app/src/main/res/menu/playlist_menu.xml index f9da37c..fd9e0f1 100644 --- a/app/src/main/res/menu/playlist_menu.xml +++ b/app/src/main/res/menu/playlist_menu.xml @@ -11,4 +11,10 @@ android:icon="@drawable/ic_baseline_edit_24" android:title="@string/edit" app:showAsAction="ifRoom" /> +