mirror of
				https://github.com/AntennaPod/AntennaPod.git
				synced 2025-10-29 19:59:22 +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