mirror of
https://github.com/AntennaPod/AntennaPod.git
synced 2025-10-29 03:36:21 +00:00
Feature copy transcript (#7914)
This commit is contained in:
parent
ed3efd0459
commit
4b101583c5
@ -1,6 +1,7 @@
|
||||
package de.danoeh.antennapod.ui.screen.playback;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -11,11 +12,14 @@ import com.google.android.material.elevation.SurfaceColors;
|
||||
import de.danoeh.antennapod.databinding.TranscriptItemBinding;
|
||||
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.model.feed.Transcript;
|
||||
import de.danoeh.antennapod.model.feed.TranscriptSegment;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.ui.common.Converter;
|
||||
import de.danoeh.antennapod.ui.transcript.TranscriptViewholder;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
@ -29,6 +33,8 @@ public class TranscriptAdapter extends RecyclerView.Adapter<TranscriptViewholder
|
||||
private FeedMedia media;
|
||||
private int prevHighlightPosition = -1;
|
||||
private int highlightPosition = -1;
|
||||
private boolean inMultiselectMode = false;
|
||||
private final HashSet<Integer> selectedPositions = new HashSet<>();
|
||||
|
||||
public TranscriptAdapter(Context context, SegmentClickListener segmentClickListener) {
|
||||
this.context = context;
|
||||
@ -49,6 +55,21 @@ public class TranscriptAdapter extends RecyclerView.Adapter<TranscriptViewholder
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setMultiselectMode(boolean multiselectMode) {
|
||||
if (this.inMultiselectMode == multiselectMode) {
|
||||
return;
|
||||
}
|
||||
this.inMultiselectMode = multiselectMode;
|
||||
if (!multiselectMode) {
|
||||
selectedPositions.clear();
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public boolean isMultiselectMode() {
|
||||
return inMultiselectMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TranscriptViewholder holder, int position) {
|
||||
if (media == null || media.getTranscript() == null) {
|
||||
@ -62,6 +83,13 @@ public class TranscriptAdapter extends RecyclerView.Adapter<TranscriptViewholder
|
||||
}
|
||||
});
|
||||
|
||||
holder.viewContent.setOnLongClickListener(v -> {
|
||||
if (segmentClickListener != null) {
|
||||
segmentClickListener.onTranscriptLongClicked(position, seg);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
String timecode = Converter.getDurationStringLong((int) seg.getStartTime());
|
||||
if (!StringUtil.isBlank(seg.getSpeaker())) {
|
||||
if (position > 0 && media.getTranscript()
|
||||
@ -84,7 +112,15 @@ public class TranscriptAdapter extends RecyclerView.Adapter<TranscriptViewholder
|
||||
holder.viewContent.setText(seg.getWords());
|
||||
}
|
||||
|
||||
if (position == highlightPosition) {
|
||||
if (inMultiselectMode) {
|
||||
highlightViewHolder(holder, selectedPositions.contains(position));
|
||||
} else {
|
||||
highlightViewHolder(holder, position == highlightPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private void highlightViewHolder(TranscriptViewholder holder, boolean highlight) {
|
||||
if (highlight) {
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
holder.viewContent.setBackgroundColor(SurfaceColors.getColorForElevation(context, 32 * density));
|
||||
holder.viewContent.setAlpha(1.0f);
|
||||
@ -140,8 +176,66 @@ public class TranscriptAdapter extends RecyclerView.Adapter<TranscriptViewholder
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
public void toggleSelection(int pos) {
|
||||
if (selectedPositions.contains(pos)) {
|
||||
selectedPositions.remove(pos);
|
||||
} else {
|
||||
selectedPositions.add(pos);
|
||||
}
|
||||
notifyItemChanged(pos);
|
||||
}
|
||||
|
||||
public void selectAll() {
|
||||
if (media == null || media.getTranscript() == null) {
|
||||
return;
|
||||
}
|
||||
selectedPositions.clear();
|
||||
int count = getItemCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
selectedPositions.add(i);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public String getSelectedText() {
|
||||
if (!inMultiselectMode) {
|
||||
return null;
|
||||
}
|
||||
Transcript transcript = media.getTranscript();
|
||||
StringBuilder ss = new StringBuilder();
|
||||
String lastSpeaker = null;
|
||||
if (selectedPositions.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
java.util.List<Integer> sortedPositions = new java.util.ArrayList<>(selectedPositions);
|
||||
java.util.Collections.sort(sortedPositions);
|
||||
int prevIndex = -2;
|
||||
for (int index : sortedPositions) {
|
||||
if (prevIndex != -2 && index != prevIndex + 1) {
|
||||
ss.append("\n[...]\n");
|
||||
}
|
||||
TranscriptSegment seg = transcript.getSegmentAt(index);
|
||||
if (!StringUtil.isBlank(seg.getSpeaker())) {
|
||||
if (ObjectUtils.notEqual(lastSpeaker, seg.getSpeaker())) {
|
||||
ss.append("\n").append(seg.getSpeaker()).append(" : ");
|
||||
lastSpeaker = seg.getSpeaker();
|
||||
}
|
||||
} else {
|
||||
lastSpeaker = null;
|
||||
}
|
||||
if (!TextUtils.isEmpty(ss) && ss.charAt(ss.length() - 1) != ' ') {
|
||||
ss.append(' ');
|
||||
}
|
||||
ss.append(seg.getWords());
|
||||
prevIndex = index;
|
||||
}
|
||||
|
||||
return ss.toString().strip();
|
||||
}
|
||||
|
||||
public interface SegmentClickListener {
|
||||
void onTranscriptClicked(int position, TranscriptSegment seg);
|
||||
|
||||
void onTranscriptLongClicked(int position, TranscriptSegment seg);
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
package de.danoeh.antennapod.ui.screen.playback;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
@ -13,6 +15,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
@ -20,6 +23,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.databinding.TranscriptDialogBinding;
|
||||
import de.danoeh.antennapod.event.MessageEvent;
|
||||
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.model.feed.Transcript;
|
||||
@ -35,7 +39,8 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
public class TranscriptDialogFragment extends DialogFragment {
|
||||
public class TranscriptDialogFragment extends DialogFragment
|
||||
implements TranscriptAdapter.SegmentClickListener {
|
||||
public static final String TAG = "TranscriptFragment";
|
||||
private TranscriptDialogBinding viewBinding;
|
||||
private PlaybackController controller;
|
||||
@ -62,7 +67,7 @@ public class TranscriptDialogFragment extends DialogFragment {
|
||||
layoutManager = new LinearLayoutManager(getContext());
|
||||
viewBinding.transcriptList.setLayoutManager(layoutManager);
|
||||
|
||||
adapter = new TranscriptAdapter(getContext(), this::transcriptClicked);
|
||||
adapter = new TranscriptAdapter(getContext(), this);
|
||||
viewBinding.transcriptList.setAdapter(adapter);
|
||||
viewBinding.transcriptList.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
@ -74,40 +79,66 @@ public class TranscriptDialogFragment extends DialogFragment {
|
||||
}
|
||||
});
|
||||
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(viewBinding.getRoot())
|
||||
.setPositiveButton(getString(R.string.close_label), null)
|
||||
.setNegativeButton(getString(R.string.refresh_label), null)
|
||||
.setTitle(R.string.transcript)
|
||||
.create();
|
||||
viewBinding.toolbar.inflateMenu(R.menu.transcript);
|
||||
viewBinding.toolbar.setOnMenuItemClickListener(this::onMenuItemClick);
|
||||
|
||||
viewBinding.followAudioCheckbox.setChecked(true);
|
||||
dialog.setOnShowListener(dialog1 -> {
|
||||
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> {
|
||||
viewBinding.progLoading.setVisibility(View.VISIBLE);
|
||||
v.setClickable(false);
|
||||
v.setEnabled(false);
|
||||
loadMediaInfo(true);
|
||||
});
|
||||
});
|
||||
viewBinding.progLoading.setVisibility(View.VISIBLE);
|
||||
doInitialScroll = true;
|
||||
|
||||
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(viewBinding.getRoot())
|
||||
.setNegativeButton(R.string.close_label, null)
|
||||
.create();
|
||||
setMultiselectMode(false);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void transcriptClicked(int pos, TranscriptSegment segment) {
|
||||
long startTime = segment.getStartTime();
|
||||
long endTime = segment.getEndTime();
|
||||
private void setMultiselectMode(boolean multiselectMode) {
|
||||
adapter.setMultiselectMode(multiselectMode);
|
||||
viewBinding.toolbar.getMenu().findItem(R.id.action_copy).setVisible(multiselectMode);
|
||||
viewBinding.toolbar.getMenu().findItem(R.id.action_cancel_copy).setVisible(multiselectMode);
|
||||
viewBinding.toolbar.getMenu().findItem(R.id.action_select_all).setVisible(multiselectMode);
|
||||
viewBinding.toolbar.getMenu().findItem(R.id.action_refresh).setVisible(!multiselectMode);
|
||||
viewBinding.followAudioCheckbox.setChecked(!multiselectMode);
|
||||
}
|
||||
|
||||
scrollToPosition(pos);
|
||||
if (!(controller.getPosition() >= startTime && controller.getPosition() <= endTime)) {
|
||||
controller.seekTo((int) startTime);
|
||||
} else {
|
||||
controller.playPause();
|
||||
private void copySelectedText() {
|
||||
String selectedText = adapter.getSelectedText();
|
||||
ClipboardManager clipboardManager = ContextCompat.getSystemService(requireContext(), ClipboardManager.class);
|
||||
if (clipboardManager != null) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(getString(R.string.transcript), selectedText));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT <= 32) {
|
||||
EventBus.getDefault().post(new MessageEvent(getString(R.string.copied_to_clipboard)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTranscriptClicked(int pos, TranscriptSegment segment) {
|
||||
if (adapter.isMultiselectMode()) {
|
||||
adapter.toggleSelection(pos);
|
||||
} else {
|
||||
long startTime = segment.getStartTime();
|
||||
long endTime = segment.getEndTime();
|
||||
|
||||
scrollToPosition(pos);
|
||||
if (!(controller.getPosition() >= startTime && controller.getPosition() <= endTime)) {
|
||||
controller.seekTo((int) startTime);
|
||||
} else {
|
||||
controller.playPause();
|
||||
}
|
||||
adapter.notifyItemChanged(pos);
|
||||
viewBinding.followAudioCheckbox.setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTranscriptLongClicked(int position, TranscriptSegment seg) {
|
||||
if (!adapter.isMultiselectMode()) {
|
||||
setMultiselectMode(true);
|
||||
adapter.toggleSelection(position);
|
||||
}
|
||||
adapter.notifyItemChanged(pos);
|
||||
viewBinding.followAudioCheckbox.setChecked(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -170,12 +201,6 @@ public class TranscriptDialogFragment extends DialogFragment {
|
||||
|
||||
viewBinding.progLoading.setVisibility(View.GONE);
|
||||
adapter.setMedia(media);
|
||||
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.INVISIBLE);
|
||||
if (!TextUtils.isEmpty(((FeedMedia) media).getItem().getTranscriptUrl())) {
|
||||
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.VISIBLE);
|
||||
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(true);
|
||||
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE).setClickable(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void scrollToPosition(int pos) {
|
||||
@ -217,4 +242,23 @@ public class TranscriptDialogFragment extends DialogFragment {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
}
|
||||
private boolean onMenuItemClick(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.action_refresh) {
|
||||
viewBinding.progLoading.setVisibility(View.VISIBLE);
|
||||
loadMediaInfo(true);
|
||||
return true;
|
||||
} else if (id == R.id.action_copy) {
|
||||
copySelectedText();
|
||||
setMultiselectMode(false);
|
||||
return true;
|
||||
} else if (id == R.id.action_cancel_copy) {
|
||||
setMultiselectMode(false);
|
||||
return true;
|
||||
} else if (id == R.id.action_select_all) {
|
||||
adapter.selectAll();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,10 +2,17 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:title="@string/transcript" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progLoading"
|
||||
android:layout_width="match_parent"
|
||||
@ -20,7 +27,6 @@
|
||||
android:layout_height="0dp"
|
||||
android:scrollIndicators="right"
|
||||
android:scrollbarStyle="outsideInset"
|
||||
android:layout_marginTop="16dp"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_weight="1"
|
||||
tools:listitem="@layout/transcript_item" />
|
||||
|
||||
23
app/src/main/res/menu/transcript.xml
Normal file
23
app/src/main/res/menu/transcript.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_refresh"
|
||||
android:title="@string/refresh_label"
|
||||
android:icon="@drawable/ic_refresh"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_copy"
|
||||
android:title="@string/copy_to_clipboard"
|
||||
android:icon="@drawable/ic_copy"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_select_all"
|
||||
android:title="@string/select_all_label"
|
||||
android:icon="@drawable/ic_select_all"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_cancel_copy"
|
||||
android:title="@string/cancel_label"
|
||||
android:icon="@drawable/ic_close"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
||||
11
ui/common/src/main/res/drawable/ic_copy.xml
Normal file
11
ui/common/src/main/res/drawable/ic_copy.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/action_icon_color"
|
||||
android:pathData="M16,1H4c-1.1,0 -2,0.9 -2,2v14h2V3h12V1zm3,4H8c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2V7c0,-1.1 -0.9,-2 -2,-2zm0,16H8V7h11v14z"/>
|
||||
</vector>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user