Add Autoplay Next Button

This commit is contained in:
saltrafael 2021-08-25 10:33:27 -03:00 committed by zeppi
parent bd64d5c9ea
commit 809136358b
6 changed files with 184 additions and 59 deletions

View file

@ -11,16 +11,12 @@ import {
import { doChangeVolume, doChangeMute, doAnalyticsView, doAnalyticsBuffer } from 'redux/actions/app';
import { selectVolume, selectMute } from 'redux/selectors/app';
import { savePosition, clearPosition, doSetPlayingUri, doPlayUri } from 'redux/actions/content';
import {
makeSelectContentPositionForUri,
selectPlayingUri,
makeSelectIsPlayerFloating,
} from 'redux/selectors/content';
import { makeSelectContentPositionForUri, selectPlayingUri, makeSelectIsPlayerFloating } from 'redux/selectors/content';
import VideoViewer from './view';
import { withRouter } from 'react-router';
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
import { selectDaemonSettings, makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings';
import { toggleVideoTheaterMode, doSetClientSetting } from 'redux/actions/settings';
import { toggleVideoTheaterMode, toggleAutoplayNext, doSetClientSetting } from 'redux/actions/settings';
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
const select = (state, props) => {
@ -71,6 +67,7 @@ const perform = (dispatch) => ({
doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
toggleAutoplayNext: () => dispatch(toggleAutoplayNext()),
setVideoPlaybackRate: (rate) => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)),
doSetPlayingUri: (uri, collectionId) => dispatch(doSetPlayingUri({ uri, collectionId })),
doPlayUri: (uri) => dispatch(doPlayUri(uri)),

View file

@ -0,0 +1,29 @@
// @flow
import type { Player } from './videojs';
import videojs from 'video.js';
class AutoplayNextButton extends videojs.getComponent('Button') {
constructor(player, options = {}, autoplay) {
super(player, options, autoplay);
this.addClass(autoplay ? 'vjs-button--autoplay-next--active' : 'vjs-button--autoplay-next');
this.controlText(autoplay ? 'Autoplay Next On' : 'Autoplay Next Off');
}
}
export function addAutoplayNextButton(player: Player, toggleAutoplayNext: () => void, autoplay: boolean) {
const controlBar = player.getChild('controlBar');
const autoplayButton = new AutoplayNextButton(
player,
{
name: 'AutoplayNextButton',
text: __('Autoplay Next'),
clickHandler: () => {
toggleAutoplayNext();
},
},
autoplay
);
controlBar.addChild(autoplayButton);
}

View file

@ -53,6 +53,7 @@ type Props = {
isAudio: boolean,
startMuted: boolean,
autoplay: boolean,
autoplaySetting: boolean,
toggleVideoTheaterMode: () => void,
adUrl: ?string,
claimId: ?string,
@ -203,6 +204,7 @@ properties for this component should be kept to ONLY those that if changed shoul
export default React.memo<Props>(function VideoJs(props: Props) {
const {
autoplay,
autoplaySetting,
startMuted,
source,
sourceType,
@ -662,6 +664,24 @@ export default React.memo<Props>(function VideoJs(props: Props) {
}
}, [videoTheaterMode]);
useEffect(() => {
const player = playerRef.current;
if (player) {
const controlBar = player.getChild('controlBar');
const autoplayButton = controlBar.getChild('AutoplayNextButton');
if (autoplaySetting) {
autoplayButton.removeClass('vjs-button--autoplay-next');
autoplayButton.addClass('vjs-button--autoplay-next--active');
autoplayButton.controlText('Autoplay Next On');
} else {
autoplayButton.removeClass('vjs-button--autoplay-next--active');
autoplayButton.addClass('vjs-button--autoplay-next');
autoplayButton.controlText('Autoplay Next Off');
}
}
}, [autoplaySetting]);
// This lifecycle hook is only called once (on mount), or when `isAudio` changes.
useEffect(() => {
const vjsElement = createVideoPlayerDOM(containerRef.current);

View file

@ -16,6 +16,7 @@ import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
import LoadingScreen from 'component/common/loading-screen';
import { addTheaterModeButton } from './internal/theater-mode';
import { addAutoplayNextButton } from './internal/autoplay-next';
import { addPlayNextButton } from './internal/play-next';
import { addPlayPreviousButton } from './internal/play-previous';
import { useGetAds } from 'effects/use-get-ads';
@ -51,6 +52,7 @@ type Props = {
savePosition: (string, number) => void,
clearPosition: (string) => void,
toggleVideoTheaterMode: () => void,
toggleAutoplayNext: () => void,
setVideoPlaybackRate: (number) => void,
doSetPlayingUri: (string, string) => void,
doPlayUri: (string) => void,
@ -92,6 +94,7 @@ function VideoViewer(props: Props) {
clearPosition,
desktopPlayStartTime,
toggleVideoTheaterMode,
toggleAutoplayNext,
setVideoPlaybackRate,
doSetPlayingUri,
doPlayUri,
@ -116,6 +119,7 @@ function VideoViewer(props: Props) {
push,
} = useHistory();
const [isPlaying, setIsPlaying] = useState(false);
const [ended, setEnded] = useState(false);
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
const vjsCallbackDataRef: any = React.useRef();
@ -132,18 +136,21 @@ function VideoViewer(props: Props) {
const [startPlayPrevious, setStartPlayPrevious] = useState(false);
const [videoNode, setVideoNode] = useState(false);
const getNavigateUrl = React.useCallback((playUri: string) => {
let navigateUrl;
if (playUri) {
navigateUrl = formatLbryUrlForWeb(playUri);
if (collectionId) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
navigateUrl = navigateUrl + `?` + collectionParams.toString();
const getNavigateUrl = React.useCallback(
(playUri: string) => {
let navigateUrl;
if (playUri) {
navigateUrl = formatLbryUrlForWeb(playUri);
if (collectionId) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
navigateUrl = navigateUrl + `?` + collectionParams.toString();
}
}
}
return navigateUrl;
}, [collectionId]);
return navigateUrl;
},
[collectionId]
);
// force everything to recent when URI changes, can cause weird corner cases otherwise (e.g. navigate while autoplay is true)
useEffect(() => {
@ -193,7 +200,19 @@ function VideoViewer(props: Props) {
setStartPlayPrevious(false);
}
}
}, [isFloating, push, doSetPlayingUri, playNextUri, doPlayUri, startPlayNext, collectionId, getNavigateUrl, videoNode, startPlayPrevious, playPreviousUri]);
}, [
isFloating,
push,
doSetPlayingUri,
playNextUri,
doPlayUri,
startPlayNext,
collectionId,
getNavigateUrl,
videoNode,
startPlayPrevious,
playPreviousUri,
]);
function doTrackingBuffered(e: Event, data: any) {
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
@ -221,22 +240,34 @@ function VideoViewer(props: Props) {
});
}
const onEnded = React.useCallback(() => {
analytics.videoIsPlaying(false);
React.useEffect(() => {
if (ended) {
analytics.videoIsPlaying(false);
if (adUrl) {
setAdUrl(null);
return;
if (adUrl) {
setAdUrl(null);
return;
}
if (embedded) {
setIsEndededEmbed(true);
} else if (autoplaySetting) {
setShowAutoplayCountdown(true);
}
clearPosition(uri);
}
if (embedded) {
setIsEndededEmbed(true);
} else if (autoplaySetting) {
setShowAutoplayCountdown(true);
}
clearPosition(uri);
}, [embedded, setIsEndededEmbed, autoplaySetting, setShowAutoplayCountdown, adUrl, setAdUrl, clearPosition, uri]);
}, [
embedded,
setIsEndededEmbed,
autoplaySetting,
setShowAutoplayCountdown,
adUrl,
setAdUrl,
clearPosition,
uri,
ended,
]);
function onPlay(player) {
setIsLoading(false);
@ -283,6 +314,8 @@ function VideoViewer(props: Props) {
if (collectionId) {
addPlayNextButton(player, () => setStartPlayNext(true));
addPlayPreviousButton(player, () => setStartPlayPrevious(true));
} else {
addAutoplayNextButton(player, toggleAutoplayNext, autoplaySetting);
}
}
@ -319,7 +352,7 @@ function VideoViewer(props: Props) {
// first play tracking, used for initializing the watchman api
player.on('tracking:firstplay', doTrackingFirstPlay);
player.on('ended', onEnded);
player.on('ended', () => setEnded(true));
player.on('play', onPlay);
player.on('pause', (event) => onPause(event, player));
player.on('dispose', (event) => onDispose(event, player));
@ -407,6 +440,7 @@ function VideoViewer(props: Props) {
startMuted={autoplayIfEmbedded}
toggleVideoTheaterMode={toggleVideoTheaterMode}
autoplay={!embedded || autoplayIfEmbedded}
autoplaySetting={autoplaySetting}
claimId={claimId}
userId={userId}
allowPreRoll={!embedded && !authenticated}

View file

@ -18,8 +18,8 @@ export const IS_MAC = process.platform === 'darwin';
const UPDATE_IS_NIGHT_INTERVAL = 5 * 60 * 1000;
export function doFetchDaemonSettings() {
return dispatch => {
Lbry.settings_get().then(settings => {
return (dispatch) => {
Lbry.settings_get().then((settings) => {
analytics.toggleInternal(settings.share_usage_data);
dispatch({
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
@ -32,11 +32,11 @@ export function doFetchDaemonSettings() {
}
export function doFindFFmpeg() {
return dispatch => {
return (dispatch) => {
dispatch({
type: LOCAL_ACTIONS.FINDING_FFMPEG_STARTED,
});
return Lbry.ffmpeg_find().then(done => {
return Lbry.ffmpeg_find().then((done) => {
dispatch(doGetDaemonStatus());
dispatch({
type: LOCAL_ACTIONS.FINDING_FFMPEG_COMPLETED,
@ -46,8 +46,8 @@ export function doFindFFmpeg() {
}
export function doGetDaemonStatus() {
return dispatch => {
return Lbry.status().then(status => {
return (dispatch) => {
return Lbry.status().then((status) => {
dispatch({
type: ACTIONS.DAEMON_STATUS_RECEIVED,
data: {
@ -72,7 +72,7 @@ export function doClearDaemonSetting(key) {
key,
};
// not if syncLocked
Lbry.settings_clear(clearKey).then(defaultSettings => {
Lbry.settings_clear(clearKey).then((defaultSettings) => {
if (SDK_SYNC_KEYS.includes(key)) {
dispatch({
type: ACTIONS.SHARED_PREFERENCE_SET,
@ -83,7 +83,7 @@ export function doClearDaemonSetting(key) {
dispatch(doWalletReconnect());
}
});
Lbry.settings_get().then(settings => {
Lbry.settings_get().then((settings) => {
analytics.toggleInternal(settings.share_usage_data);
dispatch({
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
@ -108,7 +108,7 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
key,
value: !value && value !== false ? null : value,
};
Lbry.settings_set(newSettings).then(newSetting => {
Lbry.settings_set(newSettings).then((newSetting) => {
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
dispatch({
type: ACTIONS.SHARED_PREFERENCE_SET,
@ -121,7 +121,7 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
// todo: add sdk reloadsettings() (or it happens automagically?)
}
});
Lbry.settings_get().then(settings => {
Lbry.settings_get().then((settings) => {
analytics.toggleInternal(settings.share_usage_data);
dispatch({
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
@ -170,7 +170,7 @@ export function doUpdateIsNight() {
}
export function doUpdateIsNightAsync() {
return dispatch => {
return (dispatch) => {
dispatch(doUpdateIsNight());
setInterval(() => dispatch(doUpdateIsNight()), UPDATE_IS_NIGHT_INTERVAL);
@ -201,8 +201,8 @@ export function doSetDarkTime(value, options) {
export function doGetWalletSyncPreference() {
const SYNC_KEY = 'enable-sync';
return dispatch => {
return Lbry.preference_get({ key: SYNC_KEY }).then(result => {
return (dispatch) => {
return Lbry.preference_get({ key: SYNC_KEY }).then((result) => {
const enabled = result && result[SYNC_KEY];
if (enabled !== null) {
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
@ -214,8 +214,8 @@ export function doGetWalletSyncPreference() {
export function doSetWalletSyncPreference(pref) {
const SYNC_KEY = 'enable-sync';
return dispatch => {
return Lbry.preference_set({ key: SYNC_KEY, value: pref }).then(result => {
return (dispatch) => {
return Lbry.preference_set({ key: SYNC_KEY, value: pref }).then((result) => {
const enabled = result && result[SYNC_KEY];
if (enabled !== null) {
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
@ -226,7 +226,7 @@ export function doSetWalletSyncPreference(pref) {
}
export function doPushSettingsToPrefs() {
return dispatch => {
return (dispatch) => {
return new Promise((resolve, reject) => {
dispatch({
type: LOCAL_ACTIONS.SYNC_CLIENT_SETTINGS,
@ -274,8 +274,8 @@ export function doFetchLanguage(language) {
if (settings.language !== language || (settings.loadedLanguages && !settings.loadedLanguages.includes(language))) {
// this should match the behavior/logic in index-web.html
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
.then(r => r.json())
.then(j => {
.then((r) => r.json())
.then((j) => {
window.i18n_messages[language] = j;
dispatch({
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
@ -284,7 +284,7 @@ export function doFetchLanguage(language) {
},
});
})
.catch(e => {
.catch((e) => {
dispatch({
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_FAILURE,
});
@ -324,8 +324,8 @@ export function doSetLanguage(language) {
) {
// this should match the behavior/logic in index-web.html
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
.then(r => r.json())
.then(j => {
.then((r) => r.json())
.then((j) => {
window.i18n_messages[language] = j;
dispatch({
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
@ -344,7 +344,7 @@ export function doSetLanguage(language) {
});
}
})
.catch(e => {
.catch((e) => {
window.localStorage.setItem(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE);
dispatch(doSetClientSetting(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE));
const languageName = SUPPORTED_LANGUAGES[language] ? SUPPORTED_LANGUAGES[language] : language;
@ -369,7 +369,7 @@ export function doSetAutoLaunch(value) {
}
if (value === undefined) {
launcher.isEnabled().then(isEnabled => {
launcher.isEnabled().then((isEnabled) => {
if (isEnabled) {
if (!autoLaunch) {
launcher.disable().then(() => {
@ -385,7 +385,7 @@ export function doSetAutoLaunch(value) {
}
});
} else if (value === true) {
launcher.isEnabled().then(function(isEnabled) {
launcher.isEnabled().then(function (isEnabled) {
if (!isEnabled) {
launcher.enable().then(() => {
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, true));
@ -396,7 +396,7 @@ export function doSetAutoLaunch(value) {
});
} else {
// value = false
launcher.isEnabled().then(function(isEnabled) {
launcher.isEnabled().then(function (isEnabled) {
if (isEnabled) {
launcher.disable().then(() => {
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, false));
@ -410,7 +410,7 @@ export function doSetAutoLaunch(value) {
}
export function doSetAppToTrayWhenClosed(value) {
return dispatch => {
return (dispatch) => {
window.localStorage.setItem(SETTINGS.TO_TRAY_WHEN_CLOSED, value);
dispatch(doSetClientSetting(SETTINGS.TO_TRAY_WHEN_CLOSED, value));
};
@ -424,3 +424,12 @@ export function toggleVideoTheaterMode() {
dispatch(doSetClientSetting(SETTINGS.VIDEO_THEATER_MODE, !videoTheaterMode));
};
}
export function toggleAutoplayNext() {
return (dispatch, getState) => {
const state = getState();
const autoplayNext = makeSelectClientSetting(SETTINGS.AUTOPLAY)(state);
dispatch(doSetClientSetting(SETTINGS.AUTOPLAY, !autoplayNext));
};
}

View file

@ -132,6 +132,42 @@
}
}
.vjs-button--autoplay-next.vjs-button {
@media (min-width: $breakpoint-medium) {
display: block;
order: 1;
background-repeat: no-repeat;
background-position: center;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-toggle-left'%3e%3crect x='1' y='5' width='22' height='14' rx='7' ry='7'%3e%3c/rect%3e%3ccircle cx='8' cy='12' r='3'%3e%3c/circle%3e%3c/svg%3e");
}
&:focus:not(:focus-visible) {
// Need to repeat these styles because of video.js weirdness
// see: https://github.com/lbryio/lbry-desktop/pull/5549#discussion_r580406932
background-repeat: no-repeat;
background-position: center;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-toggle-left'%3e%3crect x='1' y='5' width='22' height='14' rx='7' ry='7'%3e%3c/rect%3e%3ccircle cx='8' cy='12' r='3'%3e%3c/circle%3e%3c/svg%3e");
}
}
.vjs-button--autoplay-next--active.vjs-button {
@media (min-width: $breakpoint-medium) {
display: block;
order: 1;
background-repeat: no-repeat;
background-position: center;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-toggle-right'%3e%3crect x='1' y='5' width='22' height='14' rx='7' ry='7'%3e%3c/rect%3e%3ccircle cx='16' cy='12' r='3'%3e%3c/circle%3e%3c/svg%3e");
}
&:focus:not(:focus-visible) {
// Need to repeat these styles because of video.js weirdness
// see: https://github.com/lbryio/lbry-desktop/pull/5549#discussion_r580406932
background-repeat: no-repeat;
background-position: center;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-toggle-right'%3e%3crect x='1' y='5' width='22' height='14' rx='7' ry='7'%3e%3c/rect%3e%3ccircle cx='16' cy='12' r='3'%3e%3c/circle%3e%3c/svg%3e");
}
}
.vjs-button--play-previous.vjs-button {
@media (min-width: $breakpoint-medium) {
display: block;