beta audio capabilities

This commit is contained in:
adueck 2023-12-04 14:56:52 +04:00
parent 40c514deac
commit 77018ef252
3 changed files with 439 additions and 329 deletions

View File

@ -7,21 +7,25 @@
*/ */
import ExtraEntryInfo from "../components/ExtraEntryInfo"; import ExtraEntryInfo from "../components/ExtraEntryInfo";
import classNames from "classnames"; import classNames from "classnames";
import { import { Types as T, InlinePs } from "@lingdocs/ps-react";
Types as T,
InlinePs,
} from "@lingdocs/ps-react";
function Entry({ entry, textOptions, nonClickable, isolateEntry }: { function Entry({
entry: T.DictionaryEntry, entry,
textOptions: T.TextOptions, textOptions,
nonClickable?: boolean, nonClickable,
isolateEntry?: (ts: number) => void, isolateEntry,
}: {
entry: T.DictionaryEntry;
textOptions: T.TextOptions;
nonClickable?: boolean;
isolateEntry?: (ts: number) => void;
}) { }) {
return ( return (
<div <div
className={classNames("entry", { clickable: !nonClickable })} className={classNames("entry", { clickable: !nonClickable })}
onClick={(!nonClickable && isolateEntry) ? () => isolateEntry(entry.ts) : undefined} onClick={
!nonClickable && isolateEntry ? () => isolateEntry(entry.ts) : undefined
}
data-testid="entry" data-testid="entry"
> >
<div> <div>
@ -30,14 +34,12 @@ function Entry({ entry, textOptions, nonClickable, isolateEntry }: {
</strong> </strong>
{` `} {` `}
<em>{entry.c}</em> <em>{entry.c}</em>
{entry.a && !nonClickable && <i className="ml-2 fas fa-volume-down" />}
</div> </div>
<ExtraEntryInfo <ExtraEntryInfo entry={entry} textOptions={textOptions} />
entry={entry}
textOptions={textOptions}
/>
<div className="entry-definition">{entry.e}</div> <div className="entry-definition">{entry.e}</div>
</div> </div>
); );
}; }
export default Entry; export default Entry;

View File

@ -0,0 +1,13 @@
export default function playStorageAudio(ts: number, callback: () => void) {
if (!ts) return;
let audio = new Audio(`https://storage.lingdocs.com/${ts}.mp3`);
audio.addEventListener("ended", () => {
callback();
audio.remove();
audio.srcObject = null;
});
audio.play().catch((e) => {
console.error(e);
alert("Error playing audio - Connect to the internet and try again");
});
}

View File

@ -17,10 +17,7 @@ import {
getInflectionPattern, getInflectionPattern,
HumanReadableInflectionPattern, HumanReadableInflectionPattern,
} from "@lingdocs/ps-react"; } from "@lingdocs/ps-react";
import { import { submissionBase, addSubmission } from "../lib/submissions";
submissionBase,
addSubmission,
} from "../lib/submissions";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import Entry from "../components/Entry"; import Entry from "../components/Entry";
import Results from "../screens/Results"; import Results from "../screens/Results";
@ -35,29 +32,32 @@ import AudioPlayButton from "../components/AudioPlayButton";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Modal } from "react-bootstrap"; import { Modal } from "react-bootstrap";
import { getTextOptions } from "../lib/get-text-options"; import { getTextOptions } from "../lib/get-text-options";
import { import { entryFeeder } from "../lib/dictionary";
entryFeeder, import { State, DictionaryAPI } from "../types/dictionary-types";
} from "../lib/dictionary"; import playStorageAudio from "../components/PlayStorageAudio";
import {
State,
DictionaryAPI,
} from "../types/dictionary-types";
function IsolatedEntry({ state, dictionary, isolateEntry }: { function IsolatedEntry({
state: State, state,
dictionary: DictionaryAPI, dictionary,
isolateEntry: (ts: number) => void, isolateEntry,
}: {
state: State;
dictionary: DictionaryAPI;
isolateEntry: (ts: number) => void;
}) { }) {
const [exploded, setExploded] = useState<boolean>(false); const [exploded, setExploded] = useState<boolean>(false);
const [playing, setPlaying] = useState<boolean>(false);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const [comment, setComment] = useState<string>(""); const [comment, setComment] = useState<string>("");
const [editSubmitted, setEditSubmitted] = useState<boolean>(false); const [editSubmitted, setEditSubmitted] = useState<boolean>(false);
const [showingDeleteWarning, setShowingDeleteWarning] = useState<boolean>(false); const [showingDeleteWarning, setShowingDeleteWarning] =
useState<boolean>(false);
const [showClipped, setShowClipped] = useState<string>(""); const [showClipped, setShowClipped] = useState<string>("");
useEffect(() => { useEffect(() => {
setEditing(false); setEditing(false);
setComment(""); setComment("");
setEditSubmitted(false); setEditSubmitted(false);
setPlaying(false);
}, [state]); }, [state]);
function flashClippedMessage(m: string) { function flashClippedMessage(m: string) {
setShowClipped(m); setShowClipped(m);
@ -65,17 +65,22 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
setShowClipped(""); setShowClipped("");
}, 1250); }, 1250);
} }
const wordlistWord = state.wordlist.find((w) => w.entry.ts === state.isolatedEntry?.ts); const wordlistWord = state.wordlist.find(
(w) => w.entry.ts === state.isolatedEntry?.ts
);
const textOptions = getTextOptions(state); const textOptions = getTextOptions(state);
function submitEdit() { function submitEdit() {
if (!state.isolatedEntry) return; if (!state.isolatedEntry) return;
if (!state.user) return; if (!state.user) return;
addSubmission({ addSubmission(
{
...submissionBase(state.user), ...submissionBase(state.user),
type: "edit suggestion", type: "edit suggestion",
entry: state.isolatedEntry, entry: state.isolatedEntry,
comment, comment,
}, state.user); },
state.user
);
setEditing(false); setEditing(false);
setComment(""); setComment("");
setEditSubmitted(true); setEditSubmitted(true);
@ -96,14 +101,16 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
} }
const entry = state.isolatedEntry; const entry = state.isolatedEntry;
if (!entry) { if (!entry) {
return <div className="text-center"> return (
<div className="text-center">
<h4 className="mb-4 mt-4">Word not found</h4> <h4 className="mb-4 mt-4">Word not found</h4>
<h5><Link to="/">Home</Link></h5> <h5>
</div>; <Link to="/">Home</Link>
</h5>
</div>
);
} }
const complement = entry.l const complement = entry.l ? dictionary.findOneByTs(entry.l) : undefined;
? dictionary.findOneByTs(entry.l)
: undefined;
const relatedEntries = dictionary.findRelatedEntries(entry); const relatedEntries = dictionary.findRelatedEntries(entry);
const inf = ((): T.InflectorOutput | false => { const inf = ((): T.InflectorOutput | false => {
try { try {
@ -115,11 +122,12 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
})(); })();
const isVerbEntry = tp.isVerbEntry({ entry, complement }); const isVerbEntry = tp.isVerbEntry({ entry, complement });
function DisplayVPExplorer(props: { function DisplayVPExplorer(props: {
entry: T.DictionaryEntry, entry: T.DictionaryEntry;
complement: T.DictionaryEntry | undefined, complement: T.DictionaryEntry | undefined;
}) { }) {
try { try {
return <VPExplorer return (
<VPExplorer
verb={{ verb={{
// TODO: CLEAN THIS UP! // TODO: CLEAN THIS UP!
// @ts-ignore // @ts-ignore
@ -130,22 +138,31 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
entryFeeder={entryFeeder} entryFeeder={entryFeeder}
handleLinkClick={isolateEntry} handleLinkClick={isolateEntry}
/> />
);
} catch (e) { } catch (e) {
console.error("error rendering VPExplorer", e); console.error("error rendering VPExplorer", e);
return null; return null;
} }
} }
function handleClipId() { function handleClipId() {
if (!entry) return if (!entry) return;
navigator.clipboard.writeText(entry.ts.toString()); navigator.clipboard.writeText(entry.ts.toString());
flashClippedMessage("word id copied to clipboard"); flashClippedMessage("word id copied to clipboard");
} }
function handleClipEntry() { function handleClipEntry() {
if (!entry) return if (!entry) return;
navigator.clipboard.writeText(JSON.stringify(entry)); navigator.clipboard.writeText(JSON.stringify(entry));
flashClippedMessage("entry data copied to clipboard"); flashClippedMessage("entry data copied to clipboard");
} }
return <div className="wide-width-limiter"> function handlePlayStorageAudio() {
if (!entry) return;
setPlaying(true);
playStorageAudio(entry.ts, () => {
setPlaying(false);
});
}
return (
<div className="wide-width-limiter">
<Helmet> <Helmet>
<title>{entry.p} - LingDocs Pashto Dictionary</title> <title>{entry.p} - LingDocs Pashto Dictionary</title>
</Helmet> </Helmet>
@ -160,16 +177,24 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
</div> </div>
<div className="col-4"> <div className="col-4">
<div className="d-flex flex-row justify-content-end"> <div className="d-flex flex-row justify-content-end">
{entry.a && (
<div className="clickable mr-3" onClick={handlePlayStorageAudio}>
<i
className={`fas fa-lg fa-volume-${playing ? "down" : "off"}`}
/>
</div>
)}
<div <div
className="clickable mr-3" className="clickable mr-3"
onClick={() => setExploded(os => !os)} onClick={() => setExploded((os) => !os)}
> >
<i className={`fas fa-${exploded ? "compress" : "expand"}-alt`} /> <i className={`fas fa-${exploded ? "compress" : "expand"}-alt`} />
</div> </div>
<div className="clickable mr-3" onClick={handleClipId}> <div className="clickable mr-3" onClick={handleClipId}>
<i className="fas fa-tag"></i> <i className="fas fa-tag"></i>
</div> </div>
{state.user && state.user.level === "editor" && <> {state.user && state.user.level === "editor" && (
<>
<div className="clickable mr-3" onClick={handleClipEntry}> <div className="clickable mr-3" onClick={handleClipEntry}>
<i className="fas fa-code"></i> <i className="fas fa-code"></i>
</div> </div>
@ -181,34 +206,48 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
<i className="fa fa-gavel" /> <i className="fa fa-gavel" />
</div> </div>
</Link> </Link>
</>} </>
{state.user && <> )}
{state.user && (
<>
<div <div
className="clickable mr-3" className="clickable mr-3"
data-testid="editEntryButton" data-testid="editEntryButton"
onClick={() => setEditing(os => !os)} onClick={() => setEditing((os) => !os)}
> >
<i className="fa fa-pen" /> <i className="fa fa-pen" />
</div> </div>
{wordlistEnabled(state.user) && <div {wordlistEnabled(state.user) && (
<div
className="clickable" className="clickable"
data-testid={wordlistWord ? "fullStarButton" : "emptyStarButton"} data-testid={
onClick={wordlistWord wordlistWord ? "fullStarButton" : "emptyStarButton"
}
onClick={
wordlistWord
? () => setShowingDeleteWarning(true) ? () => setShowingDeleteWarning(true)
: () => handleAddToWordlist() : () => handleAddToWordlist()
} }
> >
<i className={`fa${wordlistWord ? "s" : "r"} fa-star fa-lg`}/> <i
</div>} className={`fa${wordlistWord ? "s" : "r"} fa-star fa-lg`}
</>} />
</div>
)}
</>
)}
</div> </div>
</div> </div>
</div> </div>
{wordlistWord && <> {wordlistWord && (
{hasAttachment(wordlistWord, "audio") && <AudioPlayButton word={wordlistWord} />} <>
{hasAttachment(wordlistWord, "audio") && (
<AudioPlayButton word={wordlistWord} />
)}
<WordlistWordEditor word={wordlistWord} /> <WordlistWordEditor word={wordlistWord} />
</>} </>
{editing && )}
{editing && (
<div className="mb-3"> <div className="mb-3">
<div className="form-group" style={{ maxWidth: "500px" }}> <div className="form-group" style={{ maxWidth: "500px" }}>
<label htmlFor="editSuggestionForm">Suggest correction/edit:</label> <label htmlFor="editSuggestionForm">Suggest correction/edit:</label>
@ -233,63 +272,108 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
<button <button
type="button" type="button"
className="btn btn-outline-secondary" className="btn btn-outline-secondary"
onClick={() => { setEditing(false); setComment("") }} onClick={() => {
setEditing(false);
setComment("");
}}
data-testid="editWordCancelButton" data-testid="editWordCancelButton"
> >
Cancel Cancel
</button> </button>
</div> </div>
</div> </div>
} )}
{editSubmitted && <p>Thank you for your help!</p>} {editSubmitted && <p>Thank you for your help!</p>}
{inf && <> {inf && (
{inf.inflections && (() => { <>
{inf.inflections &&
(() => {
const pattern = getInflectionPattern( const pattern = getInflectionPattern(
// @ts-ignore // @ts-ignore
entry entry
); );
return <div> return (
<a href={`https://grammar.lingdocs.com/inflection/inflection-patterns/${inflectionSubUrl(pattern)}`} rel="noreferrer" target="_blank"> <div>
<div className="badge bg-light mb-2">Inflection pattern {HumanReadableInflectionPattern(pattern, textOptions)} <a
href={`https://grammar.lingdocs.com/inflection/inflection-patterns/${inflectionSubUrl(
pattern
)}`}
rel="noreferrer"
target="_blank"
>
<div className="badge bg-light mb-2">
Inflection pattern{" "}
{HumanReadableInflectionPattern(pattern, textOptions)}
</div> </div>
</a> </a>
<InflectionsTable inf={inf.inflections} textOptions={textOptions} /> <InflectionsTable
</div>; inf={inf.inflections}
textOptions={textOptions}
/>
</div>
);
})()} })()}
{"plural" in inf && inf.plural !== undefined && <div> {"plural" in inf && inf.plural !== undefined && (
<div>
<h5>Plural</h5> <h5>Plural</h5>
<InflectionsTable inf={inf.plural} textOptions={textOptions} /> <InflectionsTable inf={inf.plural} textOptions={textOptions} />
</div>} </div>
{"bundledPlural" in inf && inf.bundledPlural !== undefined && <div> )}
{"bundledPlural" in inf && inf.bundledPlural !== undefined && (
<div>
<h5>Bundled Plural</h5> <h5>Bundled Plural</h5>
<InflectionsTable inf={inf.bundledPlural} textOptions={textOptions} /> <InflectionsTable
</div>} inf={inf.bundledPlural}
{"arabicPlural" in inf && inf.arabicPlural !== undefined && <div> textOptions={textOptions}
/>
</div>
)}
{"arabicPlural" in inf && inf.arabicPlural !== undefined && (
<div>
<h5>Arabic Plural</h5> <h5>Arabic Plural</h5>
<InflectionsTable inf={inf.arabicPlural} textOptions={textOptions} /> <InflectionsTable
</div>} inf={inf.arabicPlural}
</>} textOptions={textOptions}
{isVerbEntry && <div className="pb-4"> />
</div>
)}
</>
)}
{isVerbEntry && (
<div className="pb-4">
<DisplayVPExplorer entry={entry} complement={complement} /> <DisplayVPExplorer entry={entry} complement={complement} />
</div>} </div>
{showClipped && <div className="alert alert-primary text-center" role="alert" style={{ )}
{showClipped && (
<div
className="alert alert-primary text-center"
role="alert"
style={{
position: "fixed", position: "fixed",
top: "30%", top: "30%",
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
zIndex: 9999999999999, zIndex: 9999999999999,
}}> }}
>
{showClipped} {showClipped}
</div>} </div>
)}
{!!(relatedEntries && relatedEntries.length) ? <> {!!(relatedEntries && relatedEntries.length) ? (
<h4 style={{ marginTop: isVerbEntry ? "10rem" : "5rem" }}>Related Words</h4> <>
<h4 style={{ marginTop: isVerbEntry ? "10rem" : "5rem" }}>
Related Words
</h4>
<Results <Results
state={{ ...state, results: relatedEntries }} state={{ ...state, results: relatedEntries }}
isolateEntry={isolateEntry} isolateEntry={isolateEntry}
handleInflectionSearch={() => null} handleInflectionSearch={() => null}
/> />
</> : <div style={{ height: "500px" }} />} </>
) : (
<div style={{ height: "500px" }} />
)}
<Modal <Modal
show={showingDeleteWarning} show={showingDeleteWarning}
onHide={() => setShowingDeleteWarning(false)} onHide={() => setShowingDeleteWarning(false)}
@ -298,20 +382,31 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>Delete from wordlist?</Modal.Title> <Modal.Title>Delete from wordlist?</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body>Delete <InlinePs <Modal.Body>
opts={textOptions} Delete{" "}
>{{ p: entry.p, f: entry.f }}</InlinePs> from your wordlist? <InlinePs opts={textOptions}>{{ p: entry.p, f: entry.f }}</InlinePs>{" "}
from your wordlist?
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<button type="button" className="btn btn-secorndary clb" onClick={() => setShowingDeleteWarning(false)}> <button
type="button"
className="btn btn-secorndary clb"
onClick={() => setShowingDeleteWarning(false)}
>
Cancel Cancel
</button> </button>
<button type="button" data-testid="confirmDeleteFromWordlist" className="btn btn-primary clb" onClick={handleDeleteFromWordlist}> <button
type="button"
data-testid="confirmDeleteFromWordlist"
className="btn btn-primary clb"
onClick={handleDeleteFromWordlist}
>
Delete Delete
</button> </button>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
</div>; </div>
);
} }
function explodeEntry(entry: T.DictionaryEntry): T.DictionaryEntry { function explodeEntry(entry: T.DictionaryEntry): T.DictionaryEntry {
@ -334,8 +429,8 @@ function inflectionSubUrl(pattern: T.InflectionPattern): string {
? "#4-words-with-the-pashtoon-pattern" ? "#4-words-with-the-pashtoon-pattern"
: pattern === 5 : pattern === 5
? "#5-shorter-words-that-squish" ? "#5-shorter-words-that-squish"
// : pattern === 6 : // : pattern === 6
: "#6-inanimate-feminine-nouns-ending-in-ي---ee" "#6-inanimate-feminine-nouns-ending-in-ي---ee";
} }
export default IsolatedEntry; export default IsolatedEntry;