29 changed files with 1803 additions and 78 deletions
@ -0,0 +1,29 @@ |
|||||||
|
import { useSettings } from "@/state/settings"; |
||||||
|
import { Icon, Icons } from "./Icon"; |
||||||
|
|
||||||
|
export const colors = ["#ffffff", "#00ffff", "#ffff00"]; |
||||||
|
export default function CaptionColorSelector({ color }: { color: string }) { |
||||||
|
const { captionSettings, setCaptionColor } = useSettings(); |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={`flex h-8 w-8 items-center justify-center rounded transition-[background-color,transform] duration-100 hover:bg-[#1c161b79] active:scale-110 ${ |
||||||
|
color === captionSettings.style.color ? "bg-[#1C161B]" : "" |
||||||
|
}`}
|
||||||
|
onClick={() => setCaptionColor(color)} |
||||||
|
> |
||||||
|
<div |
||||||
|
className="h-4 w-4 cursor-pointer appearance-none rounded-full" |
||||||
|
style={{ |
||||||
|
backgroundColor: color, |
||||||
|
}} |
||||||
|
/> |
||||||
|
<Icon |
||||||
|
className={[ |
||||||
|
"absolute text-xs text-[#1C161B]", |
||||||
|
color === captionSettings.style.color ? "" : "hidden", |
||||||
|
].join(" ")} |
||||||
|
icon={Icons.CHECKMARK} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||||
|
import { useInterface } from "@/video/state/logic/interface"; |
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; |
||||||
|
|
||||||
|
export function VolumeAdjustedAction() { |
||||||
|
const descriptor = useVideoPlayerDescriptor(); |
||||||
|
const videoInterface = useInterface(descriptor); |
||||||
|
const mediaPlaying = useMediaPlaying(descriptor); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={[ |
||||||
|
videoInterface.volumeChangedWithKeybind |
||||||
|
? "mt-10 scale-100 opacity-100" |
||||||
|
: "mt-5 scale-75 opacity-0", |
||||||
|
"absolute left-1/2 z-[100] flex -translate-x-1/2 items-center space-x-4 rounded-full bg-bink-300 bg-opacity-50 py-2 px-5 transition-all duration-100", |
||||||
|
].join(" ")} |
||||||
|
> |
||||||
|
<Icon |
||||||
|
icon={mediaPlaying.volume > 0 ? Icons.VOLUME : Icons.VOLUME_X} |
||||||
|
className="text-xl text-white" |
||||||
|
/> |
||||||
|
<div className="h-2 w-44 overflow-hidden rounded-full bg-denim-100"> |
||||||
|
<div |
||||||
|
className="h-full rounded-r-full bg-bink-500 transition-[width] duration-100" |
||||||
|
style={{ width: `${mediaPlaying.volume * 100}%` }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,147 @@ |
|||||||
|
import { Dropdown } from "@/components/Dropdown"; |
||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { Modal, ModalCard } from "@/components/layout/Modal"; |
||||||
|
import { useSettings } from "@/state/settings"; |
||||||
|
import { useTranslation } from "react-i18next"; |
||||||
|
import { CaptionCue } from "@/video/components/actions/CaptionRendererAction"; |
||||||
|
import { |
||||||
|
CaptionLanguageOption, |
||||||
|
LangCode, |
||||||
|
captionLanguages, |
||||||
|
} from "@/setup/iso6391"; |
||||||
|
import { useMemo } from "react"; |
||||||
|
import { appLanguageOptions } from "@/setup/i18n"; |
||||||
|
import CaptionColorSelector, { |
||||||
|
colors, |
||||||
|
} from "@/components/CaptionColorSelector"; |
||||||
|
import { Slider } from "@/components/Slider"; |
||||||
|
import { conf } from "@/setup/config"; |
||||||
|
|
||||||
|
export default function SettingsModal(props: { |
||||||
|
onClose: () => void; |
||||||
|
show: boolean; |
||||||
|
}) { |
||||||
|
const { |
||||||
|
captionSettings, |
||||||
|
language, |
||||||
|
setLanguage, |
||||||
|
setCaptionLanguage, |
||||||
|
setCaptionBackgroundColor, |
||||||
|
setCaptionFontSize, |
||||||
|
} = useSettings(); |
||||||
|
const { t, i18n } = useTranslation(); |
||||||
|
|
||||||
|
const selectedCaptionLanguage = useMemo( |
||||||
|
() => captionLanguages.find((l) => l.id === captionSettings.language), |
||||||
|
[captionSettings.language] |
||||||
|
) as CaptionLanguageOption; |
||||||
|
const appLanguage = useMemo( |
||||||
|
() => appLanguageOptions.find((l) => l.id === language), |
||||||
|
[language] |
||||||
|
) as CaptionLanguageOption; |
||||||
|
const captionBackgroundOpacity = ( |
||||||
|
(parseInt(captionSettings.style.backgroundColor.substring(7, 9), 16) / |
||||||
|
255) * |
||||||
|
100 |
||||||
|
).toFixed(0); |
||||||
|
return ( |
||||||
|
<Modal show={props.show}> |
||||||
|
<ModalCard className="text-white"> |
||||||
|
<div className="flex flex-col gap-4"> |
||||||
|
<div className="flex flex-row justify-between"> |
||||||
|
<span className="text-xl font-bold">{t("settings.title")}</span> |
||||||
|
<div |
||||||
|
onClick={() => props.onClose()} |
||||||
|
className="hover:cursor-pointer" |
||||||
|
> |
||||||
|
<Icon icon={Icons.X} /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col gap-10 lg:flex-row"> |
||||||
|
<div className="lg:w-1/2"> |
||||||
|
<div className="flex flex-col justify-between"> |
||||||
|
<label className="text-md font-semibold"> |
||||||
|
{t("settings.language")} |
||||||
|
</label> |
||||||
|
<Dropdown |
||||||
|
selectedItem={appLanguage} |
||||||
|
setSelectedItem={(val) => { |
||||||
|
i18n.changeLanguage(val.id); |
||||||
|
setLanguage(val.id as LangCode); |
||||||
|
}} |
||||||
|
options={appLanguageOptions} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col justify-between"> |
||||||
|
<label className="text-md font-semibold"> |
||||||
|
{t("settings.captionLanguage")} |
||||||
|
</label> |
||||||
|
<Dropdown |
||||||
|
selectedItem={selectedCaptionLanguage} |
||||||
|
setSelectedItem={(val) => { |
||||||
|
setCaptionLanguage(val.id as LangCode); |
||||||
|
}} |
||||||
|
options={captionLanguages} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col justify-between"> |
||||||
|
<Slider |
||||||
|
label={ |
||||||
|
t( |
||||||
|
"videoPlayer.popouts.captionPreferences.fontSize" |
||||||
|
) as string |
||||||
|
} |
||||||
|
min={14} |
||||||
|
step={1} |
||||||
|
max={60} |
||||||
|
value={captionSettings.style.fontSize} |
||||||
|
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)} |
||||||
|
/> |
||||||
|
<Slider |
||||||
|
label={ |
||||||
|
t( |
||||||
|
"videoPlayer.popouts.captionPreferences.opacity" |
||||||
|
) as string |
||||||
|
} |
||||||
|
step={1} |
||||||
|
min={0} |
||||||
|
max={255} |
||||||
|
valueDisplay={`${captionBackgroundOpacity}%`} |
||||||
|
value={parseInt( |
||||||
|
captionSettings.style.backgroundColor.substring(7, 9), |
||||||
|
16 |
||||||
|
)} |
||||||
|
onChange={(e) => |
||||||
|
setCaptionBackgroundColor(e.target.valueAsNumber) |
||||||
|
} |
||||||
|
/> |
||||||
|
<div className="flex flex-row justify-between"> |
||||||
|
<label className="font-bold" htmlFor="color"> |
||||||
|
{t("videoPlayer.popouts.captionPreferences.color")} |
||||||
|
</label> |
||||||
|
<div className="flex flex-row gap-2"> |
||||||
|
{colors.map((color) => ( |
||||||
|
<CaptionColorSelector color={color} /> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div /> |
||||||
|
</div> |
||||||
|
<div className="flex w-full flex-col justify-center"> |
||||||
|
<div className="flex aspect-video flex-col justify-end rounded bg-zinc-800"> |
||||||
|
<div className="pointer-events-none flex w-full flex-col items-center transition-[bottom]"> |
||||||
|
<CaptionCue |
||||||
|
scale={0.5} |
||||||
|
text={selectedCaptionLanguage.nativeName} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="float-right mt-1 text-sm">v{conf().APP_VERSION}</div> |
||||||
|
</ModalCard> |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
} |
||||||
Loading…
Reference in new issue