Skip to content

Commit

Permalink
Inline Play/Pause button has been added.
Browse files Browse the repository at this point in the history
Voice filter has been added, only voices which support the preferred languages (browser prefs) are listed in the voice control.
  • Loading branch information
boocmp committed Sep 6, 2023
1 parent d8034df commit 118650c
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 56 deletions.
39 changes: 28 additions & 11 deletions browser/speedreader/speedreader_tab_helper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "brave/components/speedreader/speedreader_rewriter_service.h"
#include "brave/components/speedreader/speedreader_service.h"
#include "brave/components/speedreader/speedreader_util.h"
#include "brave/components/speedreader/tts_player.h"
#include "chrome/common/chrome_isolated_world_ids.h"
#include "components/dom_distiller/content/browser/distillable_page_utils.h"
#include "components/grit/brave_components_resources.h"
Expand Down Expand Up @@ -204,6 +205,17 @@ void SpeedreaderTabHelper::OnShowOriginalPage() {
TransitStateTo(DistillStates::ViewOriginal());
}

void SpeedreaderTabHelper::OnTtsPlayPause(int paragraph_index) {
auto& tts_controller =
speedreader::TtsPlayer::GetInstance()->GetControllerFor(web_contents());
if (tts_controller.IsPlaying() &&
tts_controller.IsPlayingRequestedWebContents(paragraph_index)) {
tts_controller.Pause();
} else {
tts_controller.Play(paragraph_index);
}
}

void SpeedreaderTabHelper::ClearPersistedData() {
if (auto* entry = web_contents()->GetController().GetLastCommittedEntry()) {
SpeedreaderExtendedInfoHandler::ClearPersistedData(entry);
Expand Down Expand Up @@ -234,8 +246,10 @@ void SpeedreaderTabHelper::ProcessNavigation(

auto* rewriter_service =
g_brave_browser_process->speedreader_rewriter_service();
auto* nav_entry = navigation_handle->GetNavigationEntry();

const bool url_looks_readable =
rewriter_service &&
nav_entry && !nav_entry->IsViewSourceMode() && rewriter_service &&
rewriter_service->URLLooksReadable(navigation_handle->GetURL());

const bool enabled_for_site =
Expand Down Expand Up @@ -302,19 +316,22 @@ void SpeedreaderTabHelper::DidStopLoading() {

void SpeedreaderTabHelper::DOMContentLoaded(
content::RenderFrameHost* render_frame_host) {
if (!render_frame_host->IsInPrimaryMainFrame()) {
return;
}

if (!IsPageDistillationAllowed()) {
if (!render_frame_host->IsInPrimaryMainFrame() ||
!DistillStates::IsDistilled(distill_state_)) {
return;
} else {
UpdateUI();
}
UpdateUI();

static base::NoDestructor<std::u16string> kSpeedreaderData(GetSpeedreaderData(
{{"showOriginalLinkText", IDS_READER_MODE_SHOW_ORIGINAL_PAGE_LINK},
{"minutesText", IDS_READER_MODE_MINUTES_TEXT}}));
static base::NoDestructor<std::u16string> kSpeedreaderData(
GetSpeedreaderData({
{"showOriginalLinkText", IDS_READER_MODE_SHOW_ORIGINAL_PAGE_LINK},
{"minutesText", IDS_READER_MODE_MINUTES_TEXT},
#if defined(IDS_READER_MODE_TEXT_TO_SPEECH_PLAY_PAUSE)
{
"playButtonTitle", IDS_READER_MODE_TEXT_TO_SPEECH_PLAY_PAUSE
}
#endif
}));

static base::NoDestructor<std::u16string> kJsScript(base::UTF8ToUTF16(
ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
Expand Down
1 change: 1 addition & 0 deletions browser/speedreader/speedreader_tab_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class SpeedreaderTabHelper

// mojom::SpeedreaderHost:
void OnShowOriginalPage() override;
void OnTtsPlayPause(int index) override;

private:
friend class content::WebContentsUserData<SpeedreaderTabHelper>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ SpeedreaderToolbarDataHandlerImpl::SpeedreaderToolbarDataHandlerImpl(

speedreader::TtsPlayer::GetInstance()->set_delegate(
std::make_unique<TtsPlayerDelegate>());

const auto& tts_settings = GetSpeedreaderService()->GetTtsSettings();
speedreader::TtsPlayer::GetInstance()->SetSpeed(
static_cast<double>(tts_settings.speed) / 100.0);
speedreader::TtsPlayer::GetInstance()->SetVoice(tts_settings.voice);
}

SpeedreaderToolbarDataHandlerImpl::~SpeedreaderToolbarDataHandlerImpl() =
Expand Down
3 changes: 3 additions & 0 deletions components/speedreader/common/speedreader.mojom
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ module speedreader.mojom;
interface SpeedreaderHost {
// The browser handler for clicking on the "Show original page" link.
OnShowOriginalPage();

// The browser handler for clicking on the "Play/Pause" button.
OnTtsPlayPause(int32 paragraph_index);
};
28 changes: 24 additions & 4 deletions components/speedreader/renderer/speedreader_js_handler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ void SpeedreaderJSHandler::Install(
v8::Local<v8::Value> speedreader_value =
global->Get(context, gin::StringToV8(isolate, kSpeedreader))
.ToLocalChecked();
if (!speedreader_value->IsUndefined())
if (!speedreader_value->IsUndefined()) {
return;
}

gin::Handle<SpeedreaderJSHandler> handler =
gin::CreateHandle(isolate, new SpeedreaderJSHandler(std::move(owner)));
if (handler.IsEmpty())
if (handler.IsEmpty()) {
return;
}

v8::PropertyDescriptor desc(handler.ToV8(), false);
desc.set_configurable(false);
Expand All @@ -75,13 +77,15 @@ void SpeedreaderJSHandler::Install(
gin::ObjectTemplateBuilder SpeedreaderJSHandler::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin::Wrappable<SpeedreaderJSHandler>::GetObjectTemplateBuilder(isolate)
.SetMethod("showOriginalPage", &SpeedreaderJSHandler::ShowOriginalPage);
.SetMethod("showOriginalPage", &SpeedreaderJSHandler::ShowOriginalPage)
.SetMethod("ttsPlayPause", &SpeedreaderJSHandler::TtsPlayPause);
}

void SpeedreaderJSHandler::ShowOriginalPage(v8::Isolate* isolate) {
DCHECK(isolate);
if (!owner_)
if (!owner_) {
return;
}

mojo::AssociatedRemote<speedreader::mojom::SpeedreaderHost> speedreader_host;
owner_->render_frame()->GetRemoteAssociatedInterfaces()->GetInterface(
Expand All @@ -92,4 +96,20 @@ void SpeedreaderJSHandler::ShowOriginalPage(v8::Isolate* isolate) {
}
}

void SpeedreaderJSHandler::TtsPlayPause(v8::Isolate* isolate,
int paragraph_index) {
DCHECK(isolate);
if (!owner_) {
return;
}

mojo::AssociatedRemote<speedreader::mojom::SpeedreaderHost> speedreader_host;
owner_->render_frame()->GetRemoteAssociatedInterfaces()->GetInterface(
&speedreader_host);

if (speedreader_host.is_bound()) {
speedreader_host->OnTtsPlayPause(paragraph_index);
}
}

} // namespace speedreader
2 changes: 2 additions & 0 deletions components/speedreader/renderer/speedreader_js_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class SpeedreaderJSHandler final : public gin::Wrappable<SpeedreaderJSHandler> {
// A function to be called from JS
void ShowOriginalPage(v8::Isolate* isolate);

void TtsPlayPause(v8::Isolate* isolate, int index);

base::WeakPtr<SpeedreaderRenderFrameObserver> owner_;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,22 @@ interface TtsControlProps {

function TtsControl(props: TtsControlProps) {
const [voices, setVoices] = React.useState(speechSynthesis.getVoices())
speechSynthesis.onvoiceschanged = () => {
setVoices(speechSynthesis.getVoices())
}

const [playbackState, setPlaybackState] = React.useState<PlaybackState>(PlaybackState.kStopped)
getToolbarAPI().dataHandler.getPlaybackState().then(res => setPlaybackState(res.playbackState))
getToolbarAPI().eventsRouter.setPlaybackState.addListener((state: PlaybackState) => {
setPlaybackState(state)
})

React.useEffect(() => {
const updateVoices = () => {
setVoices(speechSynthesis.getVoices().filter((v) => {
return navigator.languages.find((l) => { return v.lang.startsWith(l) })
}))
}
speechSynthesis.onvoiceschanged = updateVoices
window.onlanguagechange = updateVoices

getToolbarAPI().dataHandler.getPlaybackState().then(res => setPlaybackState(res.playbackState))
getToolbarAPI().eventsRouter.setPlaybackState.addListener((state: PlaybackState) => {
setPlaybackState(state)
})
}, [])

return (
<S.Box>
Expand Down
54 changes: 50 additions & 4 deletions components/speedreader/resources/speedreader-desktop.css
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ iframe {

html,
html[data-theme='light'] {
position: relative; !important;
position: relative !important;
scrollbar-gutter: stable;
background-color: var(--background-color);

Expand Down Expand Up @@ -647,13 +647,19 @@ html[data-content-style='text-only'] img {
display: none !important;
}

.tts-highlighted, .tts-highlighted * {
.tts-highlighted,
.tts-highlighted * {
position: relative;
}

@keyframes tts-fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
0% {
opacity: 0;
}

100% {
opacity: 1;
}
}

.tts-highlighted::after {
Expand All @@ -675,3 +681,43 @@ html[data-content-style='text-only'] img {

pointer-events: none;
}

[tts-paragraph-index] {
position: relative;
}

[tts-paragraph-index]:hover>.tts-paragraph-player {
opacity: 1;
}

.tts-paragraph-player {
opacity: 0;
position: absolute;

left: -4rem;
width: 4rem;
display: flex;
flex-direction: column;
flex-wrap: wrap;
overflow: hidden;
height: max(2rem, 100%);
flex: none;
gap: 1rem;
}

.tts-paragraph-player-button {
-webkit-mask-position: left;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 2rem;

position: relative;
display: inline-block;
background-color: var(--summary-text-color);
cursor: pointer;
width: 4rem;
height: 2rem;
}

.tts-play-icon {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none'%3E%3Cpath fill='%23687485' fill-rule='evenodd' d='M7.105 5.62a.2.2 0 0 0-.305.17v12.42a.2.2 0 0 0 .305.17l10.092-6.21a.2.2 0 0 0 0-.34L7.105 5.62ZM5.2 5.79c0-1.409 1.544-2.271 2.743-1.533l10.092 6.21a1.8 1.8 0 0 1 0 3.066l-10.092 6.21C6.744 20.481 5.2 19.62 5.2 18.21V5.79Z' clip-rule='evenodd'/%3E%3C/svg%3E");
}
76 changes: 53 additions & 23 deletions components/speedreader/resources/speedreader-desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,28 @@ const calculateReadtime = () => {

const defaultSpeedreaderData = {
showOriginalLinkText: 'View original',
playButtonTitle: 'Play/Pause',
averageWordsPerMinute: 265,
minutesText: 'min. read',
}

const extractTextToSpeak = () => {
const textTags = ['P', 'DIV', 'MAIN', 'ARTICLE', 'H1', 'H2', 'H3', 'H4', 'H5', 'STRONG', 'BLOCKQUOTE' ]
const getTextContent = (element) => {
if (!element) {
return null
}
const text = element.innerText.replace(/\n|\r +/g, ' ').trim()
if (text.length > 0) {
return text
}
return null
}

const initTextToSpeak = () => {
if (navigator.userAgentData.mobile) {
return
}

const textTags = ['P', 'DIV', 'MAIN', 'ARTICLE', 'H1', 'H2', 'H3', 'H4', 'H5', 'STRONG', 'BLOCKQUOTE', 'EM']

const extractParagraphs = (node) => {
let paragraphs = []
Expand All @@ -68,40 +84,53 @@ const extractTextToSpeak = () => {
return paragraphs
}

const getTextContent = (element) => {
if (!element) {
return null
let textToSpeak = 0

const makeParagraph = (elem) => {
if (!elem) {
return false
}
const text = element.innerText.replace(/\n|\r +/g, ' ').trim()
if (text.length > 0) {
return text
const text = getTextContent(elem)
if (text) {
elem.setAttribute('tts-paragraph-index', textToSpeak++)
return true
}
return null
return false
}

const textToSpeak = []
const title = $(metaDataDivId)?.querySelector('.title')
const titleText = getTextContent(title)
if (titleText) {
title.setAttribute('tts-paragraph-index', textToSpeak.length)
textToSpeak.push(titleText)
const createPlayer = (p) => {
const player = document.createElement('span')
player.classList.add('tts-paragraph-player')
const playButton = document.createElement('span')
playButton.classList.add('tts-paragraph-player-button', 'tts-play-icon')
playButton.title = speedreaderData.playButtonTitle
playButton.onclick = (ev) => {
window.speedreader.ttsPlayPause(parseInt(p.getAttribute('tts-paragraph-index')))
}
player.insertAdjacentElement('afterbegin', playButton)
p.insertAdjacentElement('afterbegin', player)
}

const paragraphs = extractParagraphs($(contentDivId))
makeParagraph($(metaDataDivId)?.querySelector('.title'))

for (const p of paragraphs) {
const text = getTextContent(p)
if (text) {
p.setAttribute('tts-paragraph-index', textToSpeak.length)
textToSpeak.push(text)
extractParagraphs($(contentDivId)).forEach((p) => {
if (makeParagraph(p)) {
createPlayer(p)
}
}
})
}

const extractTextToSpeak = () => {
const paragraphs = Array.from(document.querySelectorAll('[tts-paragraph-index]'))
.map((p) => {
return getTextContent(p)
})

return {
title: document.title,
author: $(metaDataDivId)?.querySelector('.author')?.textContent,
desciption: $(metaDataDivId)?.querySelector('.subhead')?.textContent,
paragraphs: textToSpeak
paragraphs: paragraphs
}
}

Expand All @@ -125,6 +154,7 @@ const main = () => {

initShowOriginalLink()
calculateReadtime()
initTextToSpeak()
}

(() => { main() })()
Loading

0 comments on commit 118650c

Please sign in to comment.