diff --git a/app/src/acad/res/values/strings.xml b/app/src/acad/res/values/strings.xml index a5ded81..08c20a4 100644 --- a/app/src/acad/res/values/strings.xml +++ b/app/src/acad/res/values/strings.xml @@ -205,6 +205,9 @@ Réactiver le compte Le compte a été mis en sourdine ! Chaînes + Sous-titres + Aucun + Sélectionner des sous-titres Nom Créer une chaîne Modifier une chaîne diff --git a/app/src/full/res/values/strings.xml b/app/src/full/res/values/strings.xml index e36172f..0c714c0 100644 --- a/app/src/full/res/values/strings.xml +++ b/app/src/full/res/values/strings.xml @@ -136,7 +136,9 @@ set_video_mode_choice set_video_minimize_choice - + Captions + Pickup captions + None Allows to change mode for playing videos (default, streaming or via a browser). diff --git a/app/src/main/java/app/fedilab/fedilabtube/PeertubeActivity.java b/app/src/main/java/app/fedilab/fedilabtube/PeertubeActivity.java index 619913f..fbedac5 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/PeertubeActivity.java +++ b/app/src/main/java/app/fedilab/fedilabtube/PeertubeActivity.java @@ -16,7 +16,6 @@ package app.fedilab.fedilabtube; import android.Manifest; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences; @@ -50,6 +49,7 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.PopupMenu; @@ -76,7 +76,9 @@ import org.jetbrains.annotations.NotNull; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; import javax.net.ssl.HttpsURLConnection; @@ -85,6 +87,7 @@ import app.fedilab.fedilabtube.client.APIResponse; import app.fedilab.fedilabtube.client.PeertubeAPI; import app.fedilab.fedilabtube.client.TLSSocketFactory; import app.fedilab.fedilabtube.client.entities.Account; +import app.fedilab.fedilabtube.client.entities.Caption; import app.fedilab.fedilabtube.client.entities.Peertube; import app.fedilab.fedilabtube.client.entities.Playlist; import app.fedilab.fedilabtube.client.entities.PlaylistElement; @@ -97,6 +100,7 @@ import app.fedilab.fedilabtube.helper.Helper; import app.fedilab.fedilabtube.sqlite.AccountDAO; import app.fedilab.fedilabtube.sqlite.PeertubeFavoritesDAO; import app.fedilab.fedilabtube.sqlite.Sqlite; +import app.fedilab.fedilabtube.viewmodel.CaptionsVM; import app.fedilab.fedilabtube.viewmodel.CommentVM; import app.fedilab.fedilabtube.viewmodel.FeedsVM; import app.fedilab.fedilabtube.viewmodel.PlaylistsVM; @@ -118,7 +122,7 @@ public class PeertubeActivity extends AppCompatActivity { private String peertubeInstance, videoId; private FullScreenMediaController.fullscreen fullscreen; private RelativeLayout loader; - private TextView peertube_view_count, peertube_playlist, peertube_bookmark, peertube_like_count, peertube_dislike_count, peertube_share, peertube_download, peertube_description, peertube_title; + private TextView peertube_view_count, peertube_playlist, peertube_bookmark, peertube_like_count, peertube_dislike_count, peertube_description, peertube_title, more_actions; private ScrollView peertube_information_container; private Peertube peertube; private PlayerView playerView; @@ -137,6 +141,8 @@ public class PeertubeActivity extends AppCompatActivity { private PlaylistsVM playlistsViewModel; private boolean playInMinimized; private boolean onStopCalled; + private List captions; + private String captionValue; public static void hideKeyboard(Activity activity) { if (activity != null && activity.getWindow() != null) { @@ -156,8 +162,8 @@ public class PeertubeActivity extends AppCompatActivity { peertube_bookmark = findViewById(R.id.peertube_bookmark); peertube_like_count = findViewById(R.id.peertube_like_count); peertube_dislike_count = findViewById(R.id.peertube_dislike_count); - peertube_share = findViewById(R.id.peertube_share); - peertube_download = findViewById(R.id.peertube_download); + more_actions = findViewById(R.id.more_actions); + peertube_description = findViewById(R.id.peertube_description); peertube_title = findViewById(R.id.peertube_title); peertube_information_container = findViewById(R.id.peertube_information_container); @@ -282,7 +288,7 @@ public class PeertubeActivity extends AppCompatActivity { playerView.setPlayer(player); loader.setVisibility(View.GONE); player.setPlayWhenReady(true); - + captions = null; } fullscreen = FullScreenMediaController.fullscreen.OFF; setFullscreen(FullScreenMediaController.fullscreen.OFF); @@ -294,6 +300,9 @@ public class PeertubeActivity extends AppCompatActivity { FeedsVM feedsViewModel = new ViewModelProvider(PeertubeActivity.this).get(FeedsVM.class); feedsViewModel.getVideo(peertubeInstance, videoId).observe(PeertubeActivity.this, this::manageVIewVideo); + + CaptionsVM captionsViewModel = new ViewModelProvider(PeertubeActivity.this).get(CaptionsVM.class); + captionsViewModel.getCaptions(videoId).observe(PeertubeActivity.this, this::manageCaptions); } public void change() { @@ -396,6 +405,15 @@ public class PeertubeActivity extends AppCompatActivity { this.fullscreen = fullscreen; } + + public void manageCaptions(APIResponse apiResponse) { + if (apiResponse == null || (apiResponse.getError() != null) || apiResponse.getCaptions() == null || apiResponse.getCaptions().size() == 0) { + return; + } + captions = apiResponse.getCaptions(); + } + + public void manageVIewVideo(APIResponse apiResponse) { if (apiResponse == null || (apiResponse.getError() != null) || apiResponse.getPeertubes() == null || apiResponse.getPeertubes().size() == 0) { @@ -599,17 +617,92 @@ public class PeertubeActivity extends AppCompatActivity { } - peertube_download.setOnClickListener(v -> { - if (Build.VERSION.SDK_INT >= 23) { - if (ContextCompat.checkSelfPermission(PeertubeActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(PeertubeActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(PeertubeActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE); - } else { - Helper.manageDownloads(PeertubeActivity.this, peertube.getFileDownloadUrl(null, PeertubeActivity.this)); - } - } else { - Helper.manageDownloads(PeertubeActivity.this, peertube.getFileDownloadUrl(null, PeertubeActivity.this)); + more_actions.setOnClickListener(view -> { + PopupMenu popup = new PopupMenu(PeertubeActivity.this, more_actions); + popup.getMenuInflater() + .inflate(R.menu.main_video, popup.getMenu()); + + if (captions == null) { + popup.getMenu().findItem(R.id.action_captions).setEnabled(false); } + popup.setOnMenuItemClickListener(item -> { + switch (item.getItemId()) { + case R.id.action_download: + if (Build.VERSION.SDK_INT >= 23) { + if (ContextCompat.checkSelfPermission(PeertubeActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(PeertubeActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(PeertubeActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE); + } else { + Helper.manageDownloads(PeertubeActivity.this, peertube.getFileDownloadUrl(null, PeertubeActivity.this)); + } + } else { + Helper.manageDownloads(PeertubeActivity.this, peertube.getFileDownloadUrl(null, PeertubeActivity.this)); + } + break; + case R.id.action_share: + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.shared_via)); + String url; + + url = "https://" + peertube.getInstance() + "/videos/watch/" + peertube.getUuid(); + boolean share_details = sharedpreferences.getBoolean(Helper.SET_SHARE_DETAILS, true); + String extra_text; + if (share_details) { + extra_text = "@" + peertube.getAccount().getAcct(); + extra_text += "\r\n\r\n" + peertube.getName(); + extra_text += "\n\n\uD83D\uDD17 " + url + "\r\n-\n"; + final String contentToot; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + contentToot = Html.fromHtml(peertube.getDescription(), Html.FROM_HTML_MODE_LEGACY).toString(); + else + contentToot = Html.fromHtml(peertube.getDescription()).toString(); + extra_text += contentToot; + } else { + extra_text = url; + } + sendIntent.putExtra(Intent.EXTRA_TEXT, extra_text); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, getString(R.string.share_with))); + break; + case R.id.action_captions: + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(PeertubeActivity.this); + if (captions == null) { + return true; + } + + String[] itemsKeyLanguage = new String[captions.size() + 1]; + String[] itemsLabelLanguage = new String[captions.size() + 1]; + itemsLabelLanguage[0] = getString(R.string.none); + itemsKeyLanguage[0] = "null"; + int i = 1; + if (captions.size() > 0) { + for (Caption caption : captions) { + Iterator> it = caption.getLanguage().entrySet().iterator(); + Map.Entry pair = it.next(); + itemsLabelLanguage[i] = pair.getValue(); + itemsKeyLanguage[i] = pair.getKey(); + i++; + } + } + dialogBuilder.setSingleChoiceItems(itemsLabelLanguage, i, (dialog, which) -> { + captionValue = itemsKeyLanguage[which]; + }); + + dialogBuilder.setOnDismissListener(dialogInterface -> { + + }); + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> dialog.dismiss()); + + androidx.appcompat.app.AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.setTitle(getString(R.string.pickup_captions)); + alertDialog.show(); + + break; + } + return true; + }); + popup.show(); }); + SQLiteDatabase db = Sqlite.getInstance(getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); List peertubes = new PeertubeFavoritesDAO(PeertubeActivity.this, db).getSinglePeertube(peertube); @@ -636,31 +729,6 @@ public class PeertubeActivity extends AppCompatActivity { peertube_bookmark.setCompoundDrawablesWithIntrinsicBounds(null, ContextCompat.getDrawable(PeertubeActivity.this, R.drawable.ic_baseline_bookmark_24), null, null); }); - peertube_share.setOnClickListener(v -> { - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.shared_via)); - String url; - - url = "https://" + peertube.getInstance() + "/videos/watch/" + peertube.getUuid(); - boolean share_details = sharedpreferences.getBoolean(Helper.SET_SHARE_DETAILS, true); - String extra_text; - if (share_details) { - extra_text = "@" + peertube.getAccount().getAcct(); - extra_text += "\r\n\r\n" + peertube.getName(); - extra_text += "\n\n\uD83D\uDD17 " + url + "\r\n-\n"; - final String contentToot; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - contentToot = Html.fromHtml(peertube.getDescription(), Html.FROM_HTML_MODE_LEGACY).toString(); - else - contentToot = Html.fromHtml(peertube.getDescription()).toString(); - extra_text += contentToot; - } else { - extra_text = url; - } - sendIntent.putExtra(Intent.EXTRA_TEXT, extra_text); - sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, getString(R.string.share_with))); - }); } @Override diff --git a/app/src/main/java/app/fedilab/fedilabtube/client/APIResponse.java b/app/src/main/java/app/fedilab/fedilabtube/client/APIResponse.java index c296154..cbeaa22 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/client/APIResponse.java +++ b/app/src/main/java/app/fedilab/fedilabtube/client/APIResponse.java @@ -17,6 +17,7 @@ package app.fedilab.fedilabtube.client; import java.util.List; import app.fedilab.fedilabtube.client.entities.Account; +import app.fedilab.fedilabtube.client.entities.Caption; import app.fedilab.fedilabtube.client.entities.Error; import app.fedilab.fedilabtube.client.entities.Instance; import app.fedilab.fedilabtube.client.entities.Peertube; @@ -37,6 +38,7 @@ public class APIResponse { private List playlists = null; private List domains = null; private List relationships = null; + private List captions = null; private Error error = null; private String since_id, max_id; private List playlistForVideos; @@ -170,4 +172,12 @@ public class APIResponse { public void setInstances(List instances) { this.instances = instances; } + + public List getCaptions() { + return captions; + } + + public void setCaptions(List captions) { + this.captions = captions; + } } diff --git a/app/src/main/java/app/fedilab/fedilabtube/client/PeertubeAPI.java b/app/src/main/java/app/fedilab/fedilabtube/client/PeertubeAPI.java index 1e60914..235781f 100644 --- a/app/src/main/java/app/fedilab/fedilabtube/client/PeertubeAPI.java +++ b/app/src/main/java/app/fedilab/fedilabtube/client/PeertubeAPI.java @@ -21,6 +21,7 @@ import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.text.Html; import android.text.SpannableString; +import android.util.Log; import org.json.JSONArray; import org.json.JSONException; @@ -46,6 +47,7 @@ import app.fedilab.fedilabtube.BuildConfig; import app.fedilab.fedilabtube.R; import app.fedilab.fedilabtube.client.entities.Account; import app.fedilab.fedilabtube.client.entities.AccountCreation; +import app.fedilab.fedilabtube.client.entities.Caption; import app.fedilab.fedilabtube.client.entities.ChannelCreation; import app.fedilab.fedilabtube.client.entities.Error; import app.fedilab.fedilabtube.client.entities.Instance; @@ -515,14 +517,16 @@ public class PeertubeAPI { while (i < jsonArray.length()) { Status status = new Status(); JSONObject comment = jsonArray.getJSONObject(i); - status.setId(comment.get("id").toString()); - status.setUri(comment.get("url").toString()); - status.setUrl(comment.get("url").toString()); + status.setId(comment.getString("id")); + status.setUri(comment.getString("url")); + status.setUrl(comment.getString("url")); status.setSensitive(false); - status.setContent(comment.get("text").toString()); - status.setIn_reply_to_id(comment.get("inReplyToCommentId").toString()); - status.setAccount(parseAccountResponsePeertube(instance, comment.getJSONObject("account"))); - status.setCreated_at(Helper.mstStringToDate(comment.get("createdAt").toString())); + status.setContent(comment.getString("text")); + status.setIn_reply_to_id(comment.getString("inReplyToCommentId")); + if (comment.has("account") && !comment.isNull("account")) { + status.setAccount(parseAccountResponsePeertube(instance, comment.getJSONObject("account"))); + } + status.setCreated_at(Helper.mstStringToDate(comment.getString("createdAt"))); status.setVisibility("public"); SpannableString spannableString; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) @@ -548,11 +552,11 @@ public class PeertubeAPI { private static Account parseAccountResponsePeertube(String instance, JSONObject resobj) { Account account = new Account(); try { - account.setId(resobj.get("id").toString()); - account.setUsername(resobj.get("name").toString()); - account.setAcct(resobj.get("name").toString() + "@" + resobj.get("host").toString()); + account.setId(resobj.getString("id")); + account.setUsername(resobj.getString("name")); + account.setAcct(resobj.getString("name") + "@" + resobj.getString("host")); account.setDisplay_name(resobj.get("displayName").toString()); - account.setHost(resobj.get("host").toString()); + account.setHost(resobj.getString("host")); if (resobj.has("createdAt")) account.setCreated_at(Helper.mstStringToDate(resobj.getString("createdAt"))); else @@ -994,6 +998,32 @@ public class PeertubeAPI { return newValues; } + + /** + * Find captions for a video + * + * @param videoId String id of the video + * @return APIResponse + */ + public APIResponse getCaptions(String videoId) { + + apiResponse = new APIResponse(); + try { + String response = new HttpsConnection(context).get(getAbsoluteUrl(String.format("/videos/%s/captions", videoId)), 60, null, prefKeyOauthTokenT); + Log.v(Helper.TAG, "response: " + response); + JSONArray jsonArray = new JSONObject(response).getJSONArray("data"); + List captions = parseCaption(jsonArray); + apiResponse.setCaptions(captions); + } catch (HttpsConnection.HttpsConnectionException e) { + e.printStackTrace(); + setError(e.getStatusCode(), e); + } catch (NoSuchAlgorithmException | IOException | KeyManagementException | JSONException e) { + e.printStackTrace(); + } + return apiResponse; + } + + /** * Returns an account * @@ -2053,6 +2083,46 @@ public class PeertubeAPI { } + /** + * Parse json response for captions + * + * @param jsonArray JSONArray + * @return List + */ + private List parseCaption(JSONArray jsonArray) { + + List captions = new ArrayList<>(); + try { + int i = 0; + while (i < jsonArray.length()) { + JSONObject resobj = jsonArray.getJSONObject(i); + Caption caption = parseCaption(resobj); + captions.add(caption); + i++; + } + } catch (JSONException e) { + setDefaultError(e); + } + return captions; + } + + private Caption parseCaption(JSONObject resobj) { + Caption caption = new Caption(); + try { + caption.setCaptionPath(resobj.getString("captionPath")); + if (resobj.has("language")) { + JSONObject resobjLang = resobj.getJSONObject("language"); + HashMap language = new HashMap<>(); + language.put(resobjLang.getString("id"), resobjLang.getString("label")); + caption.setLanguage(language); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return caption; + } + + /** * Parse json array response for instances * diff --git a/app/src/main/java/app/fedilab/fedilabtube/client/entities/Caption.java b/app/src/main/java/app/fedilab/fedilabtube/client/entities/Caption.java new file mode 100644 index 0000000..f367c81 --- /dev/null +++ b/app/src/main/java/app/fedilab/fedilabtube/client/entities/Caption.java @@ -0,0 +1,39 @@ +package app.fedilab.fedilabtube.client.entities; +/* 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 java.util.HashMap; + +public class Caption { + + private String captionPath; + private HashMap language; + + public String getCaptionPath() { + return captionPath; + } + + public void setCaptionPath(String captionPath) { + this.captionPath = captionPath; + } + + public HashMap getLanguage() { + return language; + } + + public void setLanguage(HashMap language) { + this.language = language; + } +} diff --git a/app/src/main/java/app/fedilab/fedilabtube/viewmodel/CaptionsVM.java b/app/src/main/java/app/fedilab/fedilabtube/viewmodel/CaptionsVM.java new file mode 100644 index 0000000..2fdf41b --- /dev/null +++ b/app/src/main/java/app/fedilab/fedilabtube/viewmodel/CaptionsVM.java @@ -0,0 +1,58 @@ +package app.fedilab.fedilabtube.viewmodel; +/* 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.Application; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import app.fedilab.fedilabtube.client.APIResponse; +import app.fedilab.fedilabtube.client.PeertubeAPI; + + +public class CaptionsVM extends AndroidViewModel { + private MutableLiveData apiResponseMutableLiveData; + + public CaptionsVM(@NonNull Application application) { + super(application); + } + + public LiveData getCaptions(String videoId) { + apiResponseMutableLiveData = new MutableLiveData<>(); + loadCaptions(videoId); + return apiResponseMutableLiveData; + } + + private void loadCaptions(String videoId) { + Context _mContext = getApplication().getApplicationContext(); + new Thread(() -> { + try { + PeertubeAPI peertubeAPI = new PeertubeAPI(_mContext); + APIResponse apiResponse = peertubeAPI.getCaptions(videoId); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> apiResponseMutableLiveData.setValue(apiResponse); + mainHandler.post(myRunnable); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } +} diff --git a/app/src/main/res/drawable/ic_baseline_cloud_download_24.xml b/app/src/main/res/drawable/ic_baseline_cloud_download_24.xml new file mode 100644 index 0000000..58832b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_cloud_download_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_subtitles_24.xml b/app/src/main/res/drawable/ic_baseline_subtitles_24.xml new file mode 100644 index 0000000..6a0521f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_subtitles_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_peertube.xml b/app/src/main/res/layout/activity_peertube.xml index b4ce257..07aaf97 100644 --- a/app/src/main/res/layout/activity_peertube.xml +++ b/app/src/main/res/layout/activity_peertube.xml @@ -183,23 +183,9 @@ android:layout_weight="1" tools:ignore="UselessLeaf" /> - + android:text="" + app:drawableTopCompat="@drawable/ic_baseline_more_vert_24" /> + + + + + +