mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 19:21:49 +01:00
caption settings + working settings sidebar
This commit is contained in:
parent
d8913bb2b7
commit
54cd1d52ca
@ -5,9 +5,10 @@ import { Icon, Icons } from "@/components/Icon";
|
|||||||
export function SidebarSection(props: {
|
export function SidebarSection(props: {
|
||||||
title: string;
|
title: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section className={props.className ?? ""}>
|
||||||
<p className="text-sm font-bold uppercase text-settings-sidebar-type-secondary mb-2">
|
<p className="text-sm font-bold uppercase text-settings-sidebar-type-secondary mb-2">
|
||||||
{props.title}
|
{props.title}
|
||||||
</p>
|
</p>
|
||||||
|
@ -33,7 +33,7 @@ export function ColorOption(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CaptionSetting(props: {
|
export function CaptionSetting(props: {
|
||||||
textTransformer?: (s: string) => string;
|
textTransformer?: (s: string) => string;
|
||||||
value: number;
|
value: number;
|
||||||
onChange?: (val: number) => void;
|
onChange?: (val: number) => void;
|
||||||
@ -209,7 +209,7 @@ function CaptionSetting(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const colors = ["#ffffff", "#80b1fa", "#e2e535"];
|
export const colors = ["#ffffff", "#80b1fa", "#e2e535"];
|
||||||
|
|
||||||
export function CaptionSettingsView({ id }: { id: string }) {
|
export function CaptionSettingsView({ id }: { id: string }) {
|
||||||
const router = useOverlayRouter(id);
|
const router = useOverlayRouter(id);
|
||||||
|
@ -46,10 +46,10 @@ export function CaptionCue({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className="pointer-events-none mb-1 select-none rounded px-4 py-1 text-center [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]"
|
className="pointer-events-none mb-1 select-none rounded px-4 py-1 text-center leading-normal [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]"
|
||||||
style={{
|
style={{
|
||||||
color: styling.color,
|
color: styling.color,
|
||||||
fontSize: `${(1.5 * styling.size).toFixed(2)}rem`,
|
fontSize: `${(1.5 * styling.size).toFixed(2)}em`,
|
||||||
backgroundColor: `rgba(0,0,0,${styling.backgroundOpacity.toFixed(2)})`,
|
backgroundColor: `rgba(0,0,0,${styling.backgroundOpacity.toFixed(2)})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
@ -5,8 +6,10 @@ import { getSessions } from "@/backend/accounts/sessions";
|
|||||||
import { WideContainer } from "@/components/layout/WideContainer";
|
import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||||
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { AccountActionsPart } from "@/pages/settings/AccountActionsPart";
|
import { AccountActionsPart } from "@/pages/settings/AccountActionsPart";
|
||||||
import { AccountEditPart } from "@/pages/settings/AccountEditPart";
|
import { AccountEditPart } from "@/pages/settings/AccountEditPart";
|
||||||
|
import { CaptionsPart } from "@/pages/settings/CaptionsPart";
|
||||||
import { DeviceListPart } from "@/pages/settings/DeviceListPart";
|
import { DeviceListPart } from "@/pages/settings/DeviceListPart";
|
||||||
import { RegisterCalloutPart } from "@/pages/settings/RegisterCalloutPart";
|
import { RegisterCalloutPart } from "@/pages/settings/RegisterCalloutPart";
|
||||||
import { SidebarPart } from "@/pages/settings/SidebarPart";
|
import { SidebarPart } from "@/pages/settings/SidebarPart";
|
||||||
@ -17,9 +20,16 @@ import { useThemeStore } from "@/stores/theme";
|
|||||||
import { SubPageLayout } from "./layouts/SubPageLayout";
|
import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||||
|
|
||||||
function SettingsLayout(props: { children: React.ReactNode }) {
|
function SettingsLayout(props: { children: React.ReactNode }) {
|
||||||
|
const { isMobile } = useIsMobile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WideContainer ultraWide>
|
<WideContainer ultraWide>
|
||||||
<div className="grid grid-cols-[260px,1fr] gap-12">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"grid gap-12",
|
||||||
|
isMobile ? "grid-cols-1" : "lg:grid-cols-[260px,1fr]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<SidebarPart />
|
<SidebarPart />
|
||||||
<div className="space-y-16">{props.children}</div>
|
<div className="space-y-16">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -59,15 +69,22 @@ export function SettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<Heading1 border className="!mb-0">
|
<div id="settings-account">
|
||||||
Account
|
<Heading1 border className="!mb-0">
|
||||||
</Heading1>
|
Account
|
||||||
{user.account ? (
|
</Heading1>
|
||||||
<AccountSettings account={user.account} />
|
{user.account ? (
|
||||||
) : (
|
<AccountSettings account={user.account} />
|
||||||
<RegisterCalloutPart />
|
) : (
|
||||||
)}
|
<RegisterCalloutPart />
|
||||||
<ThemePart active={activeTheme} setTheme={setTheme} />
|
)}
|
||||||
|
</div>
|
||||||
|
<div id="settings-appearance">
|
||||||
|
<ThemePart active={activeTheme} setTheme={setTheme} />
|
||||||
|
</div>
|
||||||
|
<div id="settings-captions">
|
||||||
|
<CaptionsPart />
|
||||||
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
</SubPageLayout>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
|
@ -25,7 +25,7 @@ export function AccountActionsPart() {
|
|||||||
<Heading2 border>Actions</Heading2>
|
<Heading2 border>Actions</Heading2>
|
||||||
<SolidSettingsCard
|
<SolidSettingsCard
|
||||||
paddingClass="px-6 py-12"
|
paddingClass="px-6 py-12"
|
||||||
className="grid grid-cols-2 gap-12"
|
className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-12"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Heading3>Delete account</Heading3>
|
<Heading3>Delete account</Heading3>
|
||||||
@ -34,7 +34,7 @@ export function AccountActionsPart() {
|
|||||||
can be recovered.
|
can be recovered.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end items-center">
|
<div className="flex justify-start lg:justify-end items-center">
|
||||||
<Button
|
<Button
|
||||||
theme="danger"
|
theme="danger"
|
||||||
onClick={deleteExec}
|
onClick={deleteExec}
|
||||||
|
78
src/pages/settings/CaptionsPart.tsx
Normal file
78
src/pages/settings/CaptionsPart.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
CaptionSetting,
|
||||||
|
ColorOption,
|
||||||
|
colors,
|
||||||
|
} from "@/components/player/atoms/settings/CaptionSettingsView";
|
||||||
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
|
import { CaptionCue } from "@/components/player/Player";
|
||||||
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
import { useSubtitleStore } from "@/stores/subtitles";
|
||||||
|
|
||||||
|
export function CaptionsPart() {
|
||||||
|
const styling = useSubtitleStore((s) => s.styling);
|
||||||
|
const isFullscreenPreview = false;
|
||||||
|
const updateStyling = useSubtitleStore((s) => s.updateStyling);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Heading1 border>Captions</Heading1>
|
||||||
|
<div className="grid grid-cols-[1fr,356px] gap-8">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<CaptionSetting
|
||||||
|
label="Background opacity"
|
||||||
|
max={100}
|
||||||
|
min={0}
|
||||||
|
onChange={(v) => updateStyling({ backgroundOpacity: v / 100 })}
|
||||||
|
value={styling.backgroundOpacity * 100}
|
||||||
|
textTransformer={(s) => `${s}%`}
|
||||||
|
/>
|
||||||
|
<CaptionSetting
|
||||||
|
label="Text size"
|
||||||
|
max={200}
|
||||||
|
min={1}
|
||||||
|
textTransformer={(s) => `${s}%`}
|
||||||
|
onChange={(v) => updateStyling({ size: v / 100 })}
|
||||||
|
value={styling.size * 100}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Menu.FieldTitle>Color</Menu.FieldTitle>
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
{colors.map((v) => (
|
||||||
|
<ColorOption
|
||||||
|
onClick={() => updateStyling({ color: v })}
|
||||||
|
color={v}
|
||||||
|
active={styling.color === v}
|
||||||
|
key={v}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="w-full aspect-video rounded relative overflow-hidden"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"radial-gradient(102.95% 87.07% at 100% 100%, #EEAA45 0%, rgba(165, 186, 151, 0.56) 54.69%, rgba(74, 207, 254, 0.00) 100%), linear-gradient(180deg, #48D3FF 0%, #3B27B2 100%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-white pointer-events-none absolute flex w-full flex-col items-center transition-[bottom] bottom-0 p-4">
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isFullscreenPreview
|
||||||
|
? ""
|
||||||
|
: "transform origin-bottom text-[0.5rem]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CaptionCue
|
||||||
|
// Can we keep this Dune quote 🥺
|
||||||
|
text="I must not fear. Fear is the mind-killer."
|
||||||
|
styling={styling}
|
||||||
|
overrideCasing={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,31 +1,92 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Sticky from "react-stickynode";
|
import Sticky from "react-stickynode";
|
||||||
|
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { SidebarLink, SidebarSection } from "@/components/layout/Sidebar";
|
import { SidebarLink, SidebarSection } from "@/components/layout/Sidebar";
|
||||||
import { Divider } from "@/components/utils/Divider";
|
import { Divider } from "@/components/utils/Divider";
|
||||||
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
|
const percentageVisible = 10;
|
||||||
|
|
||||||
export function SidebarPart() {
|
export function SidebarPart() {
|
||||||
|
const { isMobile } = useIsMobile();
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const hostname = location.hostname;
|
const hostname = location.hostname;
|
||||||
const rem = 16;
|
const rem = 16;
|
||||||
|
const [activeLink, setActiveLink] = useState("");
|
||||||
|
|
||||||
|
const settingLinks = [
|
||||||
|
{ text: "Account", id: "settings-account", icon: Icons.USER },
|
||||||
|
{ text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB },
|
||||||
|
{ text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS },
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function recheck() {
|
||||||
|
const windowHeight =
|
||||||
|
window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
|
||||||
|
const viewList = settingLinks
|
||||||
|
.map((link) => {
|
||||||
|
const el = document.getElementById(link.id);
|
||||||
|
if (!el) return { visible: false, link: link.id };
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
const visible = !(
|
||||||
|
Math.floor(
|
||||||
|
100 - ((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100
|
||||||
|
) < percentageVisible ||
|
||||||
|
Math.floor(
|
||||||
|
100 - ((rect.bottom - windowHeight) / rect.height) * 100
|
||||||
|
) < percentageVisible
|
||||||
|
);
|
||||||
|
|
||||||
|
return { visible, link: link.id };
|
||||||
|
})
|
||||||
|
.filter((v) => v.visible);
|
||||||
|
|
||||||
|
setActiveLink(viewList[0]?.link ?? "");
|
||||||
|
}
|
||||||
|
document.addEventListener("scroll", recheck);
|
||||||
|
recheck();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("scroll", recheck);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollTo = useCallback((id: string) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return null;
|
||||||
|
const y = el.getBoundingClientRect().top + window.scrollY;
|
||||||
|
window.scrollTo({
|
||||||
|
top: y - 120,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Sticky
|
<Sticky
|
||||||
enabled
|
enabled={!isMobile}
|
||||||
top={10 * rem} // 10rem
|
top={10 * rem} // 10rem
|
||||||
className="text-settings-sidebar-type-inactive"
|
className="text-settings-sidebar-type-inactive"
|
||||||
>
|
>
|
||||||
<SidebarSection title="Settings">
|
<div className="hidden lg:block">
|
||||||
<SidebarLink icon={Icons.WAND}>A war in my name!</SidebarLink>
|
<SidebarSection title="Settings">
|
||||||
<SidebarLink active icon={Icons.COMPRESS}>
|
{settingLinks.map((v) => (
|
||||||
TANSTAAFL
|
<SidebarLink
|
||||||
</SidebarLink>
|
icon={v.icon}
|
||||||
<SidebarLink icon={Icons.AIRPLAY}>We all float down here</SidebarLink>
|
active={v.id === activeLink}
|
||||||
<SidebarLink icon={Icons.BOOKMARK}>My skin is not my own</SidebarLink>
|
onClick={() => scrollTo(v.id)}
|
||||||
</SidebarSection>
|
>
|
||||||
<Divider />
|
{v.text}
|
||||||
|
</SidebarLink>
|
||||||
|
))}
|
||||||
|
</SidebarSection>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
<SidebarSection title="App information">
|
<SidebarSection title="App information">
|
||||||
<div className="flex justify-between items-center space-x-3">
|
<div className="flex justify-between items-center space-x-3">
|
||||||
<span>Version</span>
|
<span>Version</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Heading2 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
|
||||||
const availableThemes = [
|
const availableThemes = [
|
||||||
{
|
{
|
||||||
@ -117,7 +117,7 @@ export function ThemePart(props: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Heading2 border>Themes</Heading2>
|
<Heading1 border>Appearence</Heading1>
|
||||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-6 max-w-[700px]">
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-6 max-w-[700px]">
|
||||||
{/* default theme */}
|
{/* default theme */}
|
||||||
<ThemePreview
|
<ThemePreview
|
||||||
|
Loading…
Reference in New Issue
Block a user