Enhance link actions in Playlist (#20184)

* Enhance link actions in Playlist

* Clicking title on Mini player will open the Playlist folder of current item
* Add close button to Mini player so that users can unload playlist
* Clicking favicon will open a new tab with original page
* Add Context menu labeled 'View original page', which works just as clicking favicon
This commit is contained in:
Sangwoo Ko
2023-09-25 12:31:17 +09:00
committed by GitHub
parent 56f12dbf66
commit 3a5c702a25
17 changed files with 207 additions and 28 deletions
+3
View File
@@ -1255,6 +1255,9 @@ Or change later at <ph name="SETTINGS_EXTENIONS_LINK">$2<ex>brave://settings/ext
<message name="IDS_PLAYLIST_CONTEXT_MENU_REMOVE_FROM_PLAYLIST" desc="Context menu item on Playlist item to remove it from the folder">
Remove from playlist
</message>
<message name="IDS_PLAYLIST_CONTEXT_MENU_VIEW_ORIGINAL_PAGE" desc="Context menu item on Playlist item to open the source page in a new tab">
View original page
</message>
</if> <!-- enable_playlist_webui -->
<!--Add new items to the appropriate sections above -->
</messages>
@@ -5,6 +5,8 @@
#include "brave/browser/ui/views/side_panel/playlist/playlist_contents_wrapper.h"
#include <utility>
#include "brave/browser/ui/views/side_panel/playlist/playlist_side_panel_coordinator.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
@@ -129,6 +131,19 @@ void PlaylistContentsWrapper::ExitPictureInPicture() {
PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture();
}
void PlaylistContentsWrapper::AddNewContents(
content::WebContents* source,
std::unique_ptr<content::WebContents> new_contents,
const GURL& target_url,
WindowOpenDisposition disposition,
const blink::mojom::WindowFeatures& window_features,
bool user_gesture,
bool* was_blocked) {
static_cast<WebContentsDelegate*>(browser_view_->browser())
->AddNewContents(source, std::move(new_contents), target_url, disposition,
window_features, user_gesture, was_blocked);
}
bool PlaylistContentsWrapper::IsFullscreenForPlaylist() const {
if (auto* fullscreen_tab_helper = FullscreenWithinTabHelper::FromWebContents(
const_cast<PlaylistContentsWrapper*>(this)->web_contents())) {
@@ -6,6 +6,8 @@
#ifndef BRAVE_BROWSER_UI_VIEWS_SIDE_PANEL_PLAYLIST_PLAYLIST_CONTENTS_WRAPPER_H_
#define BRAVE_BROWSER_UI_VIEWS_SIDE_PANEL_PLAYLIST_PLAYLIST_CONTENTS_WRAPPER_H_
#include <memory>
#include "base/scoped_observation.h"
#include "brave/browser/ui/webui/playlist_ui.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_observer.h"
@@ -47,6 +49,14 @@ class PlaylistContentsWrapper
content::WebContents* web_contents) override;
void ExitPictureInPicture() override;
void AddNewContents(content::WebContents* source,
std::unique_ptr<content::WebContents> new_contents,
const GURL& target_url,
WindowOpenDisposition disposition,
const blink::mojom::WindowFeatures& window_features,
bool user_gesture,
bool* was_blocked) override;
// FullscreenObserver:
void OnFullscreenStateChanged() override;
+2
View File
@@ -50,6 +50,8 @@ void AddLocalizedStrings(content::WebUIDataSource* source) {
IDS_PLAYLIST_CONTEXT_MENU_RENAME_PLAYLIST},
{"bravePlaylistContextMenuDeletePlaylist",
IDS_PLAYLIST_CONTEXT_MENU_DELETE_PLAYLIST},
{"bravePlaylistContextMenuViewOriginalPage",
IDS_PLAYLIST_CONTEXT_MENU_VIEW_ORIGINAL_PAGE},
};
for (const auto& [name, id] : kLocalizedStrings) {
@@ -34,3 +34,5 @@ export const playPreviousItem = () => action(types.PLAYER_PLAY_PREVIOUS_ITEM)
export const toggleShuffle = () => action(types.PLAYER_TOGGLE_SHUFFLE)
export const toggleAutoPlay = () => action(types.PLAYER_TOGGLE_AUTO_PLAY)
export const unloadPlaylist = () => action(types.UNLOAD_PLAYLIST)
@@ -30,6 +30,9 @@ export type PlayerMessagePayload =
| ({
actionType: types.SELECTED_PLAYLIST_UPDATED
} & SelectedPlaylistUpdatedPayload)
| ({
actionType: types.UNLOAD_PLAYLIST
})
export default function postMessageToPlayer (payload: PlayerMessagePayload) {
const playerWindow = getPlayerWindow()
@@ -3,7 +3,10 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import { PlaylistItem } from 'gen/brave/components/playlist/common/mojom/playlist.mojom.m'
import {
Playlist,
PlaylistItem
} from 'gen/brave/components/playlist/common/mojom/playlist.mojom.m'
import { types } from '../constants/playlist_types'
import { PlayerState } from '../reducers/states'
export { types as PlaylistTypes } from '../constants/playlist_types'
@@ -16,6 +19,14 @@ type LastPlayedPositionChangedPayload = {
data: PlaylistItem
}
type GoBackToCurrentFolderPayload = {
data: { currentList: Playlist, currentItem: PlaylistItem }
}
type OpenSourcePagePayload = {
data: PlaylistItem
}
export type PlayerEventsPayload =
| ({
type: types.PLAYLIST_PLAYER_STATE_CHANGED
@@ -23,6 +34,12 @@ export type PlayerEventsPayload =
| ({
type: types.PLAYLIST_LAST_PLAYED_POSITION_OF_CURRENT_ITEM_CHANGED
} & LastPlayedPositionChangedPayload)
| ({
type: types.PLAYLIST_GO_BACK_TO_CURRENTLY_PLAYING_FOLDER
} & GoBackToCurrentFolderPayload)
| ({
type: types.PLAYLIST_OPEN_SOURCE_PAGE
} & OpenSourcePagePayload)
export function notifyEventsToTopFrame (playerState: PlayerEventsPayload) {
if (location.protocol.startsWith('chrome-untrusted:')) {
@@ -17,6 +17,7 @@ import {
useLastPlayerState,
usePlaylistEditMode
} from '../reducers/states'
import { useHistorySynchronization } from '../playerEventSink'
const HeaderWrapper = styled.header<{ isPlaylistPlayerPage: boolean }>`
position: sticky;
@@ -28,6 +29,8 @@ const HeaderWrapper = styled.header<{ isPlaylistPlayerPage: boolean }>`
`
export default function App () {
useHistorySynchronization()
const lastPlayerState = useLastPlayerState()
const editMode = usePlaylistEditMode()
@@ -5,9 +5,12 @@
import * as React from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import styled, { css } from 'styled-components'
import { PlaylistItem } from 'gen/brave/components/playlist/common/mojom/playlist.mojom.m.js'
import {
Playlist,
PlaylistItem
} from 'gen/brave/components/playlist/common/mojom/playlist.mojom.m.js'
import { color, font, radius, spacing } from '@brave/leo/tokens/css'
@@ -88,21 +91,31 @@ const FaviconAndTitle = styled.div`
gap: ${spacing.m};
`
const StyledFavicon = styled.img`
const StyledFavicon = styled.img<{ clickable: boolean }>`
padding: 3px;
width: 14px;
height: 14px;
border-radius: ${radius.s};
border: 1px solid rgba(0, 0, 0, 0.05);
background: ${color.white};
${p =>
p.clickable &&
css`
cursor: pointer;
`}
`
const StyledTitle = styled.div`
const StyledTitle = styled.div<{ clickable: boolean }>`
color: ${color.text.primary};
font: ${font.primary.large.semibold};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
${p =>
p.clickable &&
css`
cursor: pointer;
`}
`
const StyledPlayerControls = styled(PlayerControls)`
@@ -130,6 +143,9 @@ export default function Player () {
const currentItem = useSelector<ApplicationState, PlaylistItem | undefined>(
applicationState => applicationState.playerState?.currentItem
)
const currentList = useSelector<ApplicationState, Playlist | undefined>(
applicationState => applicationState.playerState?.currentList
)
const autoPlayEnabled = useAutoPlayEnabled()
@@ -168,8 +184,29 @@ export default function Player () {
currentItem?.id &&
`chrome-untrusted://playlist-data/${currentItem.id}/favicon`
}
clickable={!!currentItem}
onClick={() => {
if (currentItem) {
notifyEventsToTopFrame({
type: PlaylistTypes.PLAYLIST_OPEN_SOURCE_PAGE,
data: currentItem
})
}
}}
/>
<StyledTitle>{currentItem?.name}</StyledTitle>
<StyledTitle
clickable={!!(currentList && currentItem)}
onClick={() => {
if (currentList && currentItem) {
notifyEventsToTopFrame({
type: PlaylistTypes.PLAYLIST_GO_BACK_TO_CURRENTLY_PLAYING_FOLDER,
data: { currentList, currentItem }
})
}
}}
>
{currentItem?.name}
</StyledTitle>
</FaviconAndTitle>
<PlayerSeeker videoElement={videoElement} />
<StyledPlayerControls videoElement={videoElement} />
@@ -331,6 +331,14 @@ export function PlaylistItem ({
iconName: 'trash',
onClick: () =>
getPlaylistAPI().removeItemFromPlaylist(playlist.id!, id)
},
{
name: getLocalizedString(
'bravePlaylistContextMenuViewOriginalPage'
),
iconName: 'link-normal',
onClick: () =>
window.open(item.pageSource.url, '_blank', 'noopener noreferrer')
}
]}
onShowMenu={() => setShowingMenu(true)}
@@ -6,20 +6,24 @@
import * as React from 'react'
import styled, { css } from 'styled-components'
import { color, effect } from '@brave/leo/tokens/css'
import { color, effect, spacing } from '@brave/leo/tokens/css'
import Button from '@brave/leo/react/button'
import Icon from '@brave/leo/react/icon'
import { playlistControlsAreaHeight } from '../constants/style'
import postMessageToPlayer from '../api/playerApi'
import { types } from '../constants/player_types'
interface Props {
visible: boolean
isMiniPlayer: boolean
}
const StyledVideoFrame = styled.iframe<Props>`
${playlistControlsAreaHeight}
const VideoFrameContainer = styled.div<Props>`
position: relative;
width: 100vw;
border: none;
${playlistControlsAreaHeight}
${p =>
p.isMiniPlayer
? css`
@@ -27,7 +31,6 @@ const StyledVideoFrame = styled.iframe<Props>`
bottom: 0;
height: var(--player-controls-area-height);
z-index: 1;
border-top: 1px solid ${color.divider.subtle};
`
: css`
// 16:9 aspect ratio for video and fixed height for the controls area
@@ -43,19 +46,54 @@ const StyledVideoFrame = styled.iframe<Props>`
`}
`
export default function VideoFrame (props: Props) {
const StyledCloseButton = styled(Button)`
position: absolute;
margin: ${spacing.s};
right: 0;
`
function CloseButton () {
return (
<StyledVideoFrame
id='player'
src={
location.protocol === 'chrome-untrusted:'
? 'chrome-untrusted://playlist-player'
: 'iframe.html?id=playlist-components--video-player'
}
allow='autoplay; fullscreen;'
scrolling='no'
sandbox='allow-scripts allow-same-origin'
{...props}
/>
<StyledCloseButton
kind='plain-faint'
size='tiny'
onClick={() => {
postMessageToPlayer({ actionType: types.UNLOAD_PLAYLIST })
}}
>
<Icon name='close'></Icon>
</StyledCloseButton>
)
}
const StyledVideoFrame = styled.iframe<Pick<Props, 'isMiniPlayer'>>`
position: absolute;
width: 100vw;
height: 100%;
border: none;
${p =>
p.isMiniPlayer &&
css`
border-top: 1px solid ${color.divider.subtle};
`}
`
export default function VideoFrame (props: Props) {
return (
<VideoFrameContainer {...props}>
<StyledVideoFrame
id='player'
src={
location.protocol === 'chrome-untrusted:'
? 'chrome-untrusted://playlist-player'
: 'iframe.html?id=playlist-components--video-player'
}
allow='autoplay; fullscreen;'
scrolling='no'
sandbox='allow-scripts allow-same-origin'
isMiniPlayer={props.isMiniPlayer}
/>
{props.isMiniPlayer && <CloseButton />}
</VideoFrameContainer>
)
}
@@ -19,5 +19,7 @@ export enum types {
PLAYER_TOGGLE_AUTO_PLAY = '@@playlist/PLAYER_TOGGLE_AUTO_PLAY',
SELECTED_PLAYLIST_UPDATED = '@@playlist/SELECTED_PLAYLIST_UPDATED'
SELECTED_PLAYLIST_UPDATED = '@@playlist/SELECTED_PLAYLIST_UPDATED',
UNLOAD_PLAYLIST = '@@playlist/UNLOAD_PLAYLIST'
}
@@ -14,6 +14,10 @@ export const enum types {
PLAYLIST_SET_EDIT_MODE = '@@playlist/PLAYLIST_EDIT_MODE',
PLAYLIST_LAST_PLAYED_POSITION_OF_CURRENT_ITEM_CHANGED = '@@playlist/PLAYLIST_LAST_PLAYED_POSITION_CHANGED'
PLAYLIST_LAST_PLAYED_POSITION_OF_CURRENT_ITEM_CHANGED = '@@playlist/PLAYLIST_LAST_PLAYED_POSITION_CHANGED',
PLAYLIST_GO_BACK_TO_CURRENTLY_PLAYING_FOLDER = '@@playlist/PLAYLIST_GO_BACK_TO_CURRENTLY_PLAYING_FOLDER',
PLAYLIST_OPEN_SOURCE_PAGE= '@@playlist/PLAYLIST_OPEN_SOURCE_PAGE'
// TODO(sko) Need more actions for each events
}
@@ -17,6 +17,10 @@ export function handlePlayerMessage (payload: PlayerMessagePayload) {
getPlayerActions().selectedPlaylistUpdated(payload)
break
}
case types.UNLOAD_PLAYLIST: {
getPlayerActions().unloadPlaylist()
break
}
}
}
@@ -3,10 +3,15 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import { useHistory } from 'react-router'
import { getPlaylistAPI } from './api/api'
import { getPlaylistActions } from './api/getPlaylistActions'
import { PlayerEventsPayload } from './api/playerEventsNotifier'
import { types } from './constants/playlist_types'
import { History } from 'history'
import { useEffect } from 'react'
let history: History | undefined
function handlePlayerEvents (payload: PlayerEventsPayload) {
switch (payload.type) {
@@ -20,9 +25,33 @@ function handlePlayerEvents (payload: PlayerEventsPayload) {
payload.data.lastPlayedPosition
)
break
case types.PLAYLIST_GO_BACK_TO_CURRENTLY_PLAYING_FOLDER: {
if (history) {
const {currentList, currentItem } = payload.data
history.push(`/playlist/${currentList.id}#${currentItem.id}`)
}
break;
}
case types.PLAYLIST_OPEN_SOURCE_PAGE: {
window.open(
payload.data.pageSource.url,
'_blank',
'noopener,noreferrer'
)
}
}
}
export function useHistorySynchronization () {
const h = useHistory()
useEffect(() => {
history = h
return () => history = undefined
}, [h])
}
// Used to mirror state of Player from Playlist side.
export default function startReceivingPlayerEvents () {
window.onmessage = e => {
@@ -34,3 +63,4 @@ export default function startReceivingPlayerEvents () {
handlePlayerEvents(e.data)
}
}
@@ -32,7 +32,7 @@ const playerReducer: Reducer<PlayerState | undefined> = (
state: PlayerState | undefined,
action
) => {
if (state === undefined) {
if (state === undefined || action.type === types.UNLOAD_PLAYLIST) {
state = {
currentList: undefined,
itemsInOrder: undefined,
@@ -16,6 +16,7 @@ export type Message =
| 'bravePlaylistContextMenuRemoveFromPlaylist'
| 'bravePlaylistContextMenuRenamePlaylist'
| 'bravePlaylistContextMenuDeletePlaylist'
| 'bravePlaylistContextMenuViewOriginalPage'
export function getLocalizedString (message: Message) {
return getLocale(message)