added audio download button and tooltips for various buttons etc
This commit is contained in:
parent
3f919f609a
commit
cfeaec9550
|
@ -47,6 +47,7 @@ function Entry({
|
||||||
<i
|
<i
|
||||||
onClick={handlePlayStorageAudio}
|
onClick={handlePlayStorageAudio}
|
||||||
className="clickable ml-2 fas fa-volume-down px-1"
|
className="clickable ml-2 fas fa-volume-down px-1"
|
||||||
|
title="play audio"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,7 @@ export function EntryAudioDisplay({
|
||||||
opts: T.TextOptions;
|
opts: T.TextOptions;
|
||||||
user: LingdocsUser | undefined;
|
user: LingdocsUser | undefined;
|
||||||
}) {
|
}) {
|
||||||
|
const audioPath = getAudioPath(entry.ts);
|
||||||
if (!entry.a) {
|
if (!entry.a) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -24,23 +25,54 @@ export function EntryAudioDisplay({
|
||||||
action: `play ${entry.p} - ${entry.ts}`,
|
action: `play ${entry.p} - ${entry.ts}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDownload() {
|
||||||
|
const documentName = `${entry.p}-${entry.ts}.mp3`;
|
||||||
|
|
||||||
|
fetch(audioPath)
|
||||||
|
.then((res) => {
|
||||||
|
return res.blob();
|
||||||
|
})
|
||||||
|
.then((blob) => {
|
||||||
|
const href = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.download = documentName;
|
||||||
|
a.href = href;
|
||||||
|
a.click();
|
||||||
|
a.href = "";
|
||||||
|
})
|
||||||
|
.catch((err) => console.error(err));
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption className="mb-2 pl-2">
|
<figcaption className="mb-2 pl-2">
|
||||||
Listen to <InlinePs opts={opts}>{{ p: entry.p, f: entry.f }}</InlinePs>
|
<div>
|
||||||
|
Listen to{" "}
|
||||||
|
<InlinePs opts={opts}>{{ p: entry.p, f: entry.f }}</InlinePs>
|
||||||
|
</div>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
<audio
|
<audio
|
||||||
controls
|
controls
|
||||||
controlsList="nofullscreen"
|
controlsList="nofullscreen"
|
||||||
src={getAudioPath(entry.ts)}
|
src={audioPath}
|
||||||
preload="auto"
|
preload="auto"
|
||||||
onPlay={handlePlay}
|
onPlay={handlePlay}
|
||||||
>
|
>
|
||||||
<a href={getAudioPath(entry.ts)}>
|
{/* <a href={getAudioPath(entry.ts)}>
|
||||||
Download audio for{" "}
|
Download audio for{" "}
|
||||||
<InlinePs opts={opts}>{{ p: entry.p, f: entry.f }}</InlinePs>
|
<InlinePs opts={opts}>{{ p: entry.p, f: entry.f }}</InlinePs>
|
||||||
</a>
|
</a > */}
|
||||||
</audio>
|
</audio>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="ml-2 btn btn-light"
|
||||||
|
title="download audio"
|
||||||
|
>
|
||||||
|
<i className="fas fa-download"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,17 +9,18 @@
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { State } from "../types/dictionary-types";
|
import { State } from "../types/dictionary-types";
|
||||||
import {
|
import { OptionsAction, Language, SearchType } from "../types/dictionary-types";
|
||||||
OptionsAction,
|
|
||||||
Language,
|
|
||||||
SearchType,
|
|
||||||
} from "../types/dictionary-types";
|
|
||||||
|
|
||||||
const SearchBar = ({ state, optionsDispatch, handleSearchValueChange, onBottom }: {
|
const SearchBar = ({
|
||||||
state: State
|
state,
|
||||||
optionsDispatch: (action: OptionsAction) => void,
|
optionsDispatch,
|
||||||
handleSearchValueChange: (searchValue: string) => void,
|
handleSearchValueChange,
|
||||||
onBottom?: boolean,
|
onBottom,
|
||||||
|
}: {
|
||||||
|
state: State;
|
||||||
|
optionsDispatch: (action: OptionsAction) => void;
|
||||||
|
handleSearchValueChange: (searchValue: string) => void;
|
||||||
|
onBottom?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -34,7 +35,7 @@ const SearchBar = ({ state, optionsDispatch, handleSearchValueChange, onBottom }
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("focus", onFocus);
|
window.removeEventListener("focus", onFocus);
|
||||||
Mousetrap.unbind(["shift+space"]);
|
Mousetrap.unbind(["shift+space"]);
|
||||||
}
|
};
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
function onFocus() {
|
function onFocus() {
|
||||||
|
@ -50,30 +51,40 @@ const SearchBar = ({ state, optionsDispatch, handleSearchValueChange, onBottom }
|
||||||
onClick={() => optionsDispatch({ type: "toggleLanguage" })}
|
onClick={() => optionsDispatch({ type: "toggleLanguage" })}
|
||||||
data-testid="languageToggle"
|
data-testid="languageToggle"
|
||||||
>
|
>
|
||||||
<div aria-label={`language-choice-${language === "Pashto" ? "ps-to-en" : "en-to-ps"}`}>
|
<div
|
||||||
|
aria-label={`language-choice-${
|
||||||
|
language === "Pashto" ? "ps-to-en" : "en-to-ps"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
Ps <span className={`fa fa-arrow-${arrowDirection}`} /> En
|
Ps <span className={`fa fa-arrow-${arrowDirection}`} /> En
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
const SearchTypeToggle = ({ searchType }: { searchType: SearchType }) => {
|
const SearchTypeToggle = ({ searchType }: { searchType: SearchType }) => {
|
||||||
const icon = (searchType === "alphabetical") ? "book" : "bolt";
|
const icon = searchType === "alphabetical" ? "book" : "bolt";
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-secondary"
|
className="btn btn-outline-secondary"
|
||||||
onClick={() => optionsDispatch({ type: "toggleSearchType" })}
|
onClick={() => optionsDispatch({ type: "toggleSearchType" })}
|
||||||
data-testid="searchTypeToggle"
|
data-testid="searchTypeToggle"
|
||||||
|
title="toggle alphabetical/smart search"
|
||||||
>
|
>
|
||||||
<span className={`fa fa-${icon}`} />
|
<span className={`fa fa-${icon}`} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const placeholder = (state.options.searchType === "alphabetical" && state.options.language === "Pashto")
|
const placeholder =
|
||||||
|
state.options.searchType === "alphabetical" &&
|
||||||
|
state.options.language === "Pashto"
|
||||||
? "Browse alphabetically"
|
? "Browse alphabetically"
|
||||||
: `Search ${state.options.language === "Pashto" ? "Pashto" : "English"}`;
|
: `Search ${state.options.language === "Pashto" ? "Pashto" : "English"}`;
|
||||||
return (
|
return (
|
||||||
<nav className={`navbar bg-light${onBottom ? "" : " fixed-top"}`} style={{ zIndex: 50, width: "100%" }}>
|
<nav
|
||||||
|
className={`navbar bg-light${onBottom ? "" : " fixed-top"}`}
|
||||||
|
style={{ zIndex: 50, width: "100%" }}
|
||||||
|
>
|
||||||
<div className="form-inline my-1 my-lg-1">
|
<div className="form-inline my-1 my-lg-1">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<input
|
<input
|
||||||
|
@ -84,9 +95,12 @@ const SearchBar = ({ state, optionsDispatch, handleSearchValueChange, onBottom }
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleSearchValueChange(e.target.value);
|
handleSearchValueChange(e.target.value);
|
||||||
}}
|
}}
|
||||||
onBlur={e => {
|
onBlur={(e) => {
|
||||||
// don't loose focus/cursor if clicking on a word/star etc if searchBarStickyFocus is enabled
|
// don't loose focus/cursor if clicking on a word/star etc if searchBarStickyFocus is enabled
|
||||||
if (state.options.searchBarStickyFocus && e.relatedTarget === null) {
|
if (
|
||||||
|
state.options.searchBarStickyFocus &&
|
||||||
|
e.relatedTarget === null
|
||||||
|
) {
|
||||||
e.target.focus();
|
e.target.focus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -104,27 +118,33 @@ const SearchBar = ({ state, optionsDispatch, handleSearchValueChange, onBottom }
|
||||||
/>
|
/>
|
||||||
<span className="input-group-append">
|
<span className="input-group-append">
|
||||||
<span
|
<span
|
||||||
className={`btn btn-outline-secondary${!state.searchValue ? " unclickable" : ""} clear-search-button border-left-0 border`}
|
className={`btn btn-outline-secondary${
|
||||||
|
!state.searchValue ? " unclickable" : ""
|
||||||
|
} clear-search-button border-left-0 border`}
|
||||||
style={{ borderRadius: 0 }}
|
style={{ borderRadius: 0 }}
|
||||||
onClick={state.searchValue ? () => {
|
onClick={
|
||||||
|
state.searchValue
|
||||||
|
? () => {
|
||||||
handleSearchValueChange("");
|
handleSearchValueChange("");
|
||||||
// keep the focus on the input after pressing the X
|
// keep the focus on the input after pressing the X
|
||||||
inputRef.current && inputRef.current.focus();
|
inputRef.current && inputRef.current.focus();
|
||||||
} : undefined}
|
|
||||||
data-testid="clearButton"
|
|
||||||
>
|
|
||||||
<i className="fa fa-times" style={!state.searchValue ? { visibility: "hidden" } : {}}></i>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div className="input-group-append">
|
|
||||||
{state.options.language === "Pashto" &&
|
|
||||||
<SearchTypeToggle
|
|
||||||
searchType={state.options.searchType}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
{<LanguageToggle
|
: undefined
|
||||||
language={state.options.language}
|
}
|
||||||
/>}
|
data-testid="clearButton"
|
||||||
|
title="clear search"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-times"
|
||||||
|
style={!state.searchValue ? { visibility: "hidden" } : {}}
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div className="input-group-append" title="toggle search language">
|
||||||
|
{state.options.language === "Pashto" && (
|
||||||
|
<SearchTypeToggle searchType={state.options.searchType} />
|
||||||
|
)}
|
||||||
|
{<LanguageToggle language={state.options.language} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -176,21 +176,31 @@ function IsolatedEntry({
|
||||||
<div
|
<div
|
||||||
className="clickable mr-3"
|
className="clickable mr-3"
|
||||||
onClick={() => setExploded((os) => !os)}
|
onClick={() => setExploded((os) => !os)}
|
||||||
|
title="separate letters"
|
||||||
>
|
>
|
||||||
<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}
|
||||||
|
title="copy word id"
|
||||||
|
>
|
||||||
<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}
|
||||||
|
title="copy entry data"
|
||||||
|
>
|
||||||
<i className="fas fa-code"></i>
|
<i className="fas fa-code"></i>
|
||||||
</div>
|
</div>
|
||||||
<Link to={`/edit?id=${entry.ts}`} className="plain-link">
|
<Link to={`/edit?id=${entry.ts}`} className="plain-link">
|
||||||
<div
|
<div
|
||||||
className="clickable mr-3"
|
className="clickable mr-3"
|
||||||
data-testid="finalEditEntryButton"
|
data-testid="finalEditEntryButton"
|
||||||
|
title="edit entry"
|
||||||
>
|
>
|
||||||
<i className="fa fa-gavel" />
|
<i className="fa fa-gavel" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,12 +213,14 @@ function IsolatedEntry({
|
||||||
className="clickable mr-3"
|
className="clickable mr-3"
|
||||||
data-testid="editEntryButton"
|
data-testid="editEntryButton"
|
||||||
onClick={() => setEditing((os) => !os)}
|
onClick={() => setEditing((os) => !os)}
|
||||||
|
title="suggest edit"
|
||||||
>
|
>
|
||||||
<i className="fa fa-pen" />
|
<i className="fa fa-pen" />
|
||||||
</div>
|
</div>
|
||||||
{wordlistEnabled(state.user) && (
|
{wordlistEnabled(state.user) && (
|
||||||
<div
|
<div
|
||||||
className="clickable"
|
className="clickable"
|
||||||
|
title="add to wordlist"
|
||||||
data-testid={
|
data-testid={
|
||||||
wordlistWord ? "fullStarButton" : "emptyStarButton"
|
wordlistWord ? "fullStarButton" : "emptyStarButton"
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,6 +92,7 @@ function Results({
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onClick={startSuggestion}
|
onClick={startSuggestion}
|
||||||
|
title="create entry suggestion"
|
||||||
>
|
>
|
||||||
<i className="fas fa-plus" style={{ padding: "3px" }} />
|
<i className="fas fa-plus" style={{ padding: "3px" }} />
|
||||||
</button>
|
</button>
|
||||||
|
@ -107,6 +108,7 @@ function Results({
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onClick={handleInflectionSearch}
|
onClick={handleInflectionSearch}
|
||||||
|
title="search in inflections/conjugations"
|
||||||
>
|
>
|
||||||
<i className={inflectionSearchIcon} style={{ padding: "3px" }} />
|
<i className={inflectionSearchIcon} style={{ padding: "3px" }} />
|
||||||
</button>
|
</button>
|
||||||
|
|
Loading…
Reference in New Issue