Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display transcript text and follow along the audio #7103

Merged
merged 58 commits into from May 18, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
57b4e39
Transcript display using dialog
tonytamsf Apr 4, 2024
7ce7895
spotbug
tonytamsf Apr 17, 2024
3481270
refactor
tonytamsf Apr 17, 2024
a356b1a
remove license file
tonytamsf Apr 17, 2024
a6a9030
handle only a single speaker
tonytamsf Apr 17, 2024
5a85935
address comments 1
tonytamsf Apr 17, 2024
78a6e4e
checkstyle
tonytamsf Apr 17, 2024
1e27245
checkstyle
tonytamsf Apr 17, 2024
5c524a9
address comment 2
tonytamsf Apr 17, 2024
a446807
checkstyle
tonytamsf Apr 17, 2024
f91cb38
checkstyle
tonytamsf Apr 17, 2024
c69b265
checkstyle
tonytamsf Apr 17, 2024
31cd9d2
checkstyle
tonytamsf Apr 18, 2024
d75dae3
unit test changes after changes to 5 second segments
tonytamsf Apr 18, 2024
c17fff6
working using adapter
tonytamsf Apr 18, 2024
22afa76
fix bad rv viewholder when scrolling transcript fast
tonytamsf Apr 18, 2024
6383ceb
force network load of transcript
tonytamsf Apr 18, 2024
fbd00f6
checkstyle
tonytamsf Apr 18, 2024
fba119b
checkstyle
tonytamsf Apr 18, 2024
0b6774e
remove custom setting of background for dialog
tonytamsf Apr 19, 2024
c436f43
revert
tonytamsf Apr 19, 2024
6df781e
review feedback
tonytamsf Apr 19, 2024
299ba0d
checkstyle
tonytamsf Apr 19, 2024
1bb0785
checkstyle
tonytamsf Apr 19, 2024
d0e7d7a
Design tweaks
ByteHamster Apr 19, 2024
a184d6a
checkstyle
tonytamsf Apr 20, 2024
102f469
checkstyle
tonytamsf Apr 20, 2024
1374580
do not begin a line in transcript with a non-alphanumeric character
tonytamsf Apr 20, 2024
c255e95
be more careful about the last json object
tonytamsf Apr 20, 2024
adacf23
checkstyle
tonytamsf Apr 20, 2024
0f4ff71
revert after fixing offline mode
tonytamsf Apr 20, 2024
3ff78c4
Use simple binary search to replace 3 different data structures
ByteHamster Apr 20, 2024
df5899f
Empty-Commit
tonytamsf Apr 21, 2024
f45c339
unit test fixes for transcript parsing
tonytamsf Apr 21, 2024
99bc969
do not start a sentence with non alpha characters
tonytamsf Apr 22, 2024
1d1f857
reverse logic for whether a transcript segment starts with a non alph…
tonytamsf Apr 22, 2024
88e4a66
support follow audio option
tonytamsf Apr 22, 2024
7c9b356
checkstyle
tonytamsf Apr 22, 2024
d5f0385
checkstyle
tonytamsf Apr 22, 2024
8b22dd4
checkstyle
tonytamsf Apr 22, 2024
520332b
checkstyle
tonytamsf Apr 22, 2024
adb90f0
more subtle hiding of the follow audio checkbox
tonytamsf Apr 22, 2024
9262eec
Simplify if condition
ByteHamster Apr 27, 2024
3f77cfb
Backport: Switch Emulator CI to Ubuntu (#7140)
ByteHamster Apr 27, 2024
f934aeb
follow audio using layout
tonytamsf May 11, 2024
14ca190
fix followAudio checkbox overlapping with recyclerview in transcript
tonytamsf May 11, 2024
70fc3bd
fix progress loading
tonytamsf May 11, 2024
3dae14e
fix unit test for transcript
tonytamsf May 11, 2024
e520c0a
uncomment one unit test
tonytamsf May 11, 2024
d8feec8
refactor and move TranscriptUtils
tonytamsf May 11, 2024
c8be6fb
minor UI feedback on transcripts
tonytamsf May 14, 2024
b7e1335
xmllint
tonytamsf May 14, 2024
75a1adb
checkstyle
tonytamsf May 14, 2024
f501f41
Simplify some code
ByteHamster May 14, 2024
3d09a7c
Don't start another network request when chapter loading is interrupted
ByteHamster May 15, 2024
f831924
Scroll more quickly when smooth scrolling after quick scroll
ByteHamster May 15, 2024
7758393
Code simplification
ByteHamster May 15, 2024
4b70862
disable refresh when downloading so we do not repeately hit the button
tonytamsf May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Expand Up @@ -69,6 +69,7 @@ dependencies {
implementation project(':net:ssl')
implementation project(':net:sync:service')
implementation project(':parser:feed')
implementation project(':parser:transcript')
implementation project(':playback:base')
implementation project(':playback:cast')
implementation project(':storage:database')
Expand Down
Expand Up @@ -60,6 +60,7 @@ public static boolean onPrepareMenu(Menu menu, FeedItem selectedItem) {
final boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists();
final boolean isLocalFile = hasMedia && selectedItem.getFeed().isLocalFeed();
final boolean isFavorite = selectedItem.isTagged(FeedItem.TAG_FAVORITE);
final boolean hasTranscript = selectedItem.hasTranscript();

setItemVisibility(menu, R.id.skip_episode_item, isPlaying);
setItemVisibility(menu, R.id.remove_from_queue_item, isInQueue);
Expand All @@ -84,6 +85,7 @@ public static boolean onPrepareMenu(Menu menu, FeedItem selectedItem) {
setItemVisibility(menu, R.id.add_to_favorites_item, !isFavorite);
setItemVisibility(menu, R.id.remove_from_favorites_item, isFavorite);
setItemVisibility(menu, R.id.remove_item, fileDownloaded || isLocalFile);
setItemVisibility(menu, R.id.transcript_item, hasTranscript);
return true;
}

Expand Down
@@ -0,0 +1,130 @@
package de.danoeh.antennapod.ui.screen.playback;

import static java.security.AccessController.getContext;

Check failure on line 3 in app/src/main/java/de/danoeh/antennapod/ui/screen/playback/TranscriptAdapter.java

View workflow job for this annotation

GitHub Actions / Static Code Analysis

Checkstyle

Unused import - java.security.AccessController.getContext.

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.jsoup.internal.StringUtil;

import de.danoeh.antennapod.databinding.FragmentItemTranscriptRvBinding;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.model.feed.Transcript;
import de.danoeh.antennapod.model.feed.TranscriptSegment;
import de.danoeh.antennapod.parser.transcript.TranscriptParser;
import de.danoeh.antennapod.ui.transcript.TranscriptViewholder;

import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

/**
* {@link RecyclerView.Adapter} that can display a {@link PlaceholderItem}.
* TODO: Replace the implementation with code for your data type.
*/
public class TranscriptAdapter extends RecyclerView.Adapter<TranscriptViewholder> {

public String tag = "ItemTranscriptRVAdapter";
public Hashtable<Long, Integer> positions;
public Hashtable<Integer, TranscriptSegment> snippets;

private Transcript transcript;

public TranscriptAdapter(Transcript t) {
positions = new Hashtable<Long, Integer>();
snippets = new Hashtable<Integer, TranscriptSegment>();
setTranscript(t);
}

@NonNull
@Override
public TranscriptViewholder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
return new TranscriptViewholder(FragmentItemTranscriptRvBinding.inflate(LayoutInflater.from(viewGroup.getContext()),

Check failure on line 51 in app/src/main/java/de/danoeh/antennapod/ui/screen/playback/TranscriptAdapter.java

View workflow job for this annotation

GitHub Actions / Static Code Analysis

Checkstyle

Line is longer than 120 characters (found 124).
viewGroup,
false));

}

public void setTranscript(Transcript t) {
transcript = t;
if (transcript == null) {
return;
}
TreeMap<Long, TranscriptSegment> segmentsMap = transcript.getSegmentsMap();
Object[] objs = segmentsMap.entrySet().toArray();
for (int i = 0; i < objs.length; i++) {
Map.Entry<Long, TranscriptSegment> seg;
seg = (Map.Entry<Long, TranscriptSegment>) objs[i];
positions.put((Long) seg.getKey(), i);
snippets.put(i, seg.getValue());
}
}


@Override
public void onBindViewHolder(@NonNull TranscriptViewholder holder, int position) {
TreeMap<Long, TranscriptSegment> segmentsMap;
SortedMap<Long, TranscriptSegment> map;

segmentsMap = transcript.getSegmentsMap();
// TODO: fix this performance problem with getting a new Array
TreeMap.Entry entry = (TreeMap.Entry) segmentsMap.entrySet().toArray()[position];
TranscriptSegment seg = (TranscriptSegment) entry.getValue();
Long k = (Long) entry.getKey();

Log.d(tag, "onBindTranscriptViewholder position " + position + " RV pos " + k);
holder.transcriptSegment = seg;
holder.viewTimecode.setText(TranscriptParser.secondsToTime(k));
holder.viewTimecode.setVisibility(View.GONE);
Set<String> speakers = transcript.getSpeakers();

if (! StringUtil.isBlank(seg.getSpeaker())) {
TreeMap.Entry prevEntry = null;
try {
prevEntry = (TreeMap.Entry) segmentsMap.entrySet().toArray()[position - 1];
} catch (ArrayIndexOutOfBoundsException e) {
Log.d(tag, "ArrayIndexOutOfBoundsException");
}
TranscriptSegment prevSeg = null;
if (prevEntry != null) {
prevSeg = (TranscriptSegment) prevEntry.getValue();
}
if (prevEntry != null && prevSeg.getSpeaker().equals(seg.getSpeaker()) ) {

Check failure on line 101 in app/src/main/java/de/danoeh/antennapod/ui/screen/playback/TranscriptAdapter.java

View workflow job for this annotation

GitHub Actions / Static Code Analysis

Checkstyle

')' is preceded with whitespace.
holder.viewTimecode.setVisibility(View.GONE);
holder.viewContent.setText(seg.getWords());
} else {
holder.viewTimecode.setVisibility(View.VISIBLE);
holder.viewTimecode.setText(TranscriptParser.secondsToTime(k) + " " + seg.getSpeaker());
holder.viewContent.setText(seg.getWords());
}
} else {
if (speakers.size() <= 0 && (position % 5 == 0)) {
holder.viewTimecode.setVisibility(View.VISIBLE);
holder.viewTimecode.setText(TranscriptParser.secondsToTime(k));
}
holder.viewContent.setText(seg.getWords());
}
}

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
Log.d(tag, "onEventMainThread ItemTranscriptRVAdapter");
}

@Override
public int getItemCount() {
if (transcript == null) {
return 0;
}
return transcript.getSegmentsMap().size();
}
}
@@ -0,0 +1,235 @@
package de.danoeh.antennapod.ui.screen.playback;

import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;

import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;

import de.danoeh.antennapod.playback.service.PlaybackController;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.ui.chapters.PodcastIndexTranscriptUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import de.danoeh.antennapod.R;
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.ThemeUtils;

public class TranscriptFragment extends AppCompatDialogFragment {
tonytamsf marked this conversation as resolved.
Show resolved Hide resolved
public static final String TAG = "TranscriptFragment";
RecyclerView rv;
tonytamsf marked this conversation as resolved.
Show resolved Hide resolved
private ProgressBar progressBar;

private PlaybackController controller;

Transcript transcript;
SortedMap<Long, TranscriptSegment> map;
TreeMap<Long, TranscriptSegment> segmentsMap;
tonytamsf marked this conversation as resolved.
Show resolved Hide resolved
TranscriptAdapter adapter = null;
View currentView = null;
View prevView = null;
Color prevColor;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setStyle(STYLE_NO_TITLE, 0);
}

@Override
public void onResume() {
ViewGroup.LayoutParams params;
params = getDialog().getWindow().getAttributes();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
getDialog().getWindow().setAttributes((WindowManager.LayoutParams) params);
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
tonytamsf marked this conversation as resolved.
Show resolved Hide resolved
super.onResume();
}

@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
.setView(onCreateView(getLayoutInflater()))
.setPositiveButton(getString(R.string.close_label), null) //dismisses
.create();
dialog.show();

return dialog;
}

public View onCreateView(LayoutInflater inflater) {
Log.d(TAG, "Creating view");
View root = inflater.inflate(R.layout.fragment_item_transcript_rv_list, null, false);
rv = root.findViewById(R.id.transcript_list);
rv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "Clicked");

}
});

LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
layoutManager.setRecycleChildrenOnDetach(true);
rv.setLayoutManager(layoutManager);

controller = new PlaybackController(getActivity()) {
tonytamsf marked this conversation as resolved.
Show resolved Hide resolved
@Override
public void loadMediaInfo() {
load();
}
};
controller.init();
// Set the adapter
Context context = rv.getContext();
RecyclerView recyclerView = (RecyclerView) rv;
recyclerView.setLayoutManager(new LinearLayoutManager(context));

Playable media = controller.getMedia();
if (media != null && media instanceof FeedMedia) {
FeedMedia feedMedia = ((FeedMedia) media);
if (feedMedia.getItem() == null) {
feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId()));
}

transcript = PodcastIndexTranscriptUtils.loadTranscript(feedMedia);
if (transcript != null) {
segmentsMap = transcript.getSegmentsMap();
adapter = new TranscriptAdapter(transcript);
recyclerView.setAdapter(adapter);
}
}
return root;
}

@Override
public void onStart() {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
public void loadMediaInfo() {
// TODO
}
};
controller.init();
EventBus.getDefault().register(this);
}


@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "Fragment destroyed");
if (rv != null) {
rv.removeAllViews();
tonytamsf marked this conversation as resolved.
Show resolved Hide resolved
}
}

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
Log.d(TAG, "onEventMainThread TranscriptFragment " + event.getPosition());
scrollToPosition(event.getPosition());
}


private void load() {
Log.d(TAG, "load()");
Context context = getContext();
if (context == null) {
return;
}
}

@Override
public void onPause() {
super.onPause();
}

@SuppressLint("ResourceAsColor")
public void scrollToPosition(long position) {
if (segmentsMap == null) {
return;
}
Map.Entry<Long, TranscriptSegment> entry = segmentsMap.floorEntry(position);
if (entry != null) {
Integer pos = adapter.positions.get(entry.getKey());
if (pos != null) {
Log.d(TAG, "Scrolling to position" + pos + " jump " + Long.toString(entry.getKey()));
final LinearSmoothScroller smoothScroller = new LinearSmoothScroller(getActivity()) {
@Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
};

rv.scrollToPosition(0);
rv.getLayoutManager().scrollToPosition(pos);
rv.setTop(pos - 1);
smoothScroller.setTargetPosition(pos - 1); // pos on which item you want to scroll recycler view
rv.getLayoutManager().startSmoothScroll(smoothScroller);
tonytamsf marked this conversation as resolved.
Show resolved Hide resolved

View nextView = rv.getLayoutManager().findViewByPosition(pos);
if (nextView != null && nextView != currentView) {
prevView = currentView;
currentView = nextView;
}
if (currentView != null) {
((TextView) currentView.findViewById(R.id.content)).setTypeface(null, Typeface.BOLD);
((TextView) currentView.findViewById(R.id.content)).setTextColor(
ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary)
);
}

if (prevView != null && prevView != currentView && currentView != null) {
((TextView) prevView.findViewById(R.id.content)).setTypeface(null, Typeface.NORMAL);
((TextView) prevView.findViewById(R.id.content)).setTextColor(
ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorSecondary));
}
tonytamsf marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

public void scrollToTop() {
rv.getLayoutManager().scrollToPosition(0);
}

@Override
public void onStop() {
super.onStop();
controller.release();
controller = null;
EventBus.getDefault().unregister(this);
}

}