Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions frontend/scenarios/set_video_segment_by_clicks.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#language: fr

Fonctionnalité: Définir un segment vidéo par deux clics

Scénario: Le bouton n'est visible que si la vidéo a une annotation ouverte
Soit une page affichant une vidéo avec l'API YouTube simulée
Et la vidéo n'a pas d'annotation ouverte
Et une session active avec mon compte
Alors le bouton "Define segment start" n'est pas visible

Scénario: Le bouton apparaît quand une annotation est ouverte
Soit une page affichant une vidéo avec l'API YouTube simulée
Et une session active avec mon compte
Et la vidéo contient une annotation ouverte
Alors le bouton "Define segment start" est visible sous la vidéo

Scénario: 1er clic capture le début, 2e clic capture la fin et ouvre l'éditeur
Soit une page affichant une vidéo avec l'API YouTube simulée
Et une session active avec mon compte
Et la vidéo contient une annotation ouverte
Quand je positionne la vidéo à "00:03:09.000"
Et j'appuie sur le bouton "Define segment start"
Alors le bouton affiche "Define segment end (start: 00:03:09.000)"
Et l'éditeur de commentaire n'est pas ouvert
Quand je positionne la vidéo à "00:03:15.000"
Et j'appuie sur le bouton "Define segment end (start: 00:03:09.000)"
Alors l'éditeur de commentaire s'ouvre avec le timecode "00:03:09.000 --> 00:03:15.000"

Scénario: Annuler après le premier clic permet de recommencer
Soit une page affichant une vidéo avec l'API YouTube simulée
Et une session active avec mon compte
Et la vidéo contient une annotation ouverte
Quand je positionne la vidéo à "00:01:00.000"
Et j'appuie sur le bouton "Define segment start"
Alors le bouton affiche "Define segment end (start: 00:01:00.000)"
Quand j'annule la définition du segment
Alors le bouton revient à son état initial "Define segment start"

Scénario: Deux clics rapprochés fonctionnent normalement
Soit une page affichant une vidéo avec l'API YouTube simulée
Et une session active avec mon compte
Et la vidéo contient une annotation ouverte
Quand je positionne la vidéo à "00:05:00.000"
Et j'appuie deux fois rapidement sur le bouton "Define segment start"
Alors l'éditeur de commentaire s'ouvre avec le timecode "00:05:00.000 --> 00:05:00.000"
21 changes: 18 additions & 3 deletions frontend/src/components/EditableText.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import '../styles/EditableText.css';

import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useContext } from 'react';
import FormattedText from './FormattedText';
import DiscreeteDropdown from './DiscreeteDropdown';
import PictureUploadAction from '../menu-items/PictureUploadAction';
import {v4 as uuid} from 'uuid';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { SegmentContext } from './SegmentContext';

function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, setHighlightedText, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate}) {
function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment, setHighlightedText, setSelectedText, rawEditMode, setRawEditMode, backend, setLastUpdate, isSegmentReceiver}) {
const [beingEdited, setBeingEdited] = useState(false);
const [editedDocument, setEditedDocument] = useState();
const [editedText, setEditedText] = useState();
const [hasBeenChanged, setHasBeenChanged] = useState(false);
const { segmentTimecode, setSegmentTimecode } = useContext(SegmentContext);
const PASSAGE = new RegExp(`\\{${rubric}} ?([^{]*)`);

let parsePassage = (rawText) => (rubric)
Expand Down Expand Up @@ -46,6 +48,19 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
}
}, [fragment, parseFirstPassage, setFragment, updateEditedDocument]);

useEffect(() => {
if (segmentTimecode && isSegmentReceiver) {
updateEditedDocument()
.then((x) => {
let existingText = parseFirstPassage(x.text);
setEditedText((existingText && `${existingText}\n\n`) + segmentTimecode);
setBeingEdited(true);
setSegmentTimecode(null);
setHasBeenChanged(true);
});
}
}, [segmentTimecode, isSegmentReceiver, parseFirstPassage, setSegmentTimecode, updateEditedDocument]);

useEffect(() => {
if (rawEditMode) {
updateEditedDocument()
Expand Down Expand Up @@ -128,4 +143,4 @@ function EditableText({id, text, rubric, isPartOf, links, fragment, setFragment,
);
}

export default EditableText;
export default EditableText;
36 changes: 16 additions & 20 deletions frontend/src/components/FormattedText.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { remarkDefinitionList, defListHastHandlers } from 'remark-definition-lis
import CroppedImage from './CroppedImage';
import VideoComment from './VideoComment';
import FragmentComment from './FragmentComment';
import VideoPlayer from './VideoPlayer';

function FormattedText({children, setHighlightedText, selectable, setSelectedText}) {
function FormattedText({children, setHighlightedText, selectable, setSelectedText, showSegmentControls}) {

const handleMouseUp = () => {
if (selectable) {
Expand All @@ -16,11 +17,23 @@ function FormattedText({children, setHighlightedText, selectable, setSelectedTex
}
};

function getVideoId(src) {
const regExp = /^.*(?:youtube\.com\/watch\?v=|youtu\.be\/)([^\s&]{11})/;
const match = src.match(regExp);
return match ? match[1] : null;
}

return (<>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkDefinitionList, remarkUnwrapImages]}
components={{
img: (x) => embedVideo(x) || CroppedImage(x),
img: (x) => {
let videoId = getVideoId(x.src);
if (videoId) {
return <VideoPlayer videoId={videoId} showSegmentControls={showSegmentControls} />;
}
return CroppedImage(x);
},
p: (x) => VideoComment(x)
|| FragmentComment({...x, setHighlightedText})
|| <p onMouseUp={handleMouseUp}>{x.children}</p>,
Expand All @@ -35,21 +48,4 @@ function FormattedText({children, setHighlightedText, selectable, setSelectedTex
</>);
}

function getId(text) {
const regExp = /^.*(?:youtube\.com\/watch\?v=|youtu\.be\/)([^\s&]{11})/;
const match = text.match(regExp);
return match ? match[1] : null;
}

function embedVideo({src}) {
const videoId = getId(src);
if (videoId) {
const embedLink = `https://www.youtube.com/embed/${videoId}`;
return (
<iframe width="80%" height="300" src={embedLink} frameBorder="0" allowFullScreen></iframe>
);
}
return null;
}

export default FormattedText;
export default FormattedText;
19 changes: 16 additions & 3 deletions frontend/src/components/Passage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '../styles/Passage.css';

import { useState } from 'react';
import { useState, useEffect, useContext } from 'react';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
Expand All @@ -9,12 +9,25 @@ import FormattedText from './FormattedText';
import EditableText from '../components/EditableText';
import DiscreeteDropdown from './DiscreeteDropdown';
import CommentFragmentAction from '../menu-items/CommentFragmentAction';
import { SegmentContext } from './SegmentContext';

function Passage({source, rubric, scholia, margin, sourceId, isComposite, rawEditMode, setRawEditMode, backend, setLastUpdate}) {
const [selectedText, setSelectedText] = useState();
const [highlightedText, setHighlightedText] = useState('');
const [fragment, setFragment] = useState();
const isFromScratch = margin === sourceId;
const { segmentTimecode, setSegmentTimecode } = useContext(SegmentContext);

const hasVideo = source.some(
chunk => /(?:youtube\.com\/watch\?v=|youtu\.be\/)/.test(chunk)
);

useEffect(() => {
if (segmentTimecode && hasVideo) {
setFragment(segmentTimecode + '\n\n');
setSegmentTimecode(null);
}
}, [segmentTimecode, hasVideo, setFragment, setSegmentTimecode]);

scholia = scholia.filter(x => (x.isPartOf === margin));
if (!scholia.length) {
Expand Down Expand Up @@ -74,7 +87,7 @@ function PassageSource({children, isComposite, highlightedText, setHighlightedTe
function SelectableFormattedText({children, highlightedText, setHighlightedText, setSelectedText}) {
return (
<Marker mark={highlightedText} options={({separateWordSearch: false})}>
<FormattedText selectable="true" {...{setSelectedText, setHighlightedText}}>
<FormattedText selectable="true" showSegmentControls={true} {...{setSelectedText, setHighlightedText}}>
{children}
</FormattedText>
</Marker>
Expand All @@ -98,4 +111,4 @@ function PassageMargin({active, scholia, setHighlightedText, fragment, setFragme
);
}

export default Passage;
export default Passage;
3 changes: 3 additions & 0 deletions frontend/src/components/SegmentContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from 'react';

export const SegmentContext = createContext({});
34 changes: 24 additions & 10 deletions frontend/src/components/VideoComment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,39 @@ function VideoComment({ children }) {
}
className="videoComment"
>
{children[0]}
{children[0].replace(/ @\S+$/, '')}
</p>
</OverlayTrigger>
);

function playVideoAt(timecode) {
let [start, end] = timecode.split('-->');
function playVideoAt(timecodeString) {
let videoIdMatch = timecodeString.match(/@(\S+)/);
let targetVideoId = videoIdMatch ? videoIdMatch[1] : null;

let [start, end] = timecodeString.split('-->');
let [hour, min, sec] = start.split(/[:.]/);
let startTime = Number(hour * 3600) + Number(min * 60) + Number(sec);
[hour, min, sec] = end.split(/[:.]/);
let endTime = Number(hour * 3600) + Number(min * 60) + Number(sec);
let iframe = document.getElementsByTagName('iframe');
if (iframe.length != 0) {
let youTubeLink = new URL(iframe[0].src);
let youTubeBaseLink = youTubeLink.origin + youTubeLink.pathname;
let targetLink = `${youTubeBaseLink}?start=${startTime}&end=${endTime}&autoplay=1&mute=1`;
iframe[0].src = targetLink;

let iframes = document.getElementsByTagName('iframe');
let iframe;

if (targetVideoId) {
iframe = Array.from(iframes).find(
f => f.getAttribute('data-video-id') === targetVideoId
);
}
if (!iframe && iframes.length !== 0) {
iframe = iframes[0];
}

if (iframe) {
let videoIdForUrl = targetVideoId || new URL(iframe.src).pathname.split('/').pop();
let targetLink = `https://www.youtube.com/embed/${videoIdForUrl}?start=${startTime}&end=${endTime}&autoplay=1&mute=1`;
iframe.src = targetLink;
}
}
}

export default VideoComment;
export default VideoComment;
Loading
Loading