mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 19:41:50 +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: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<section className={props.className ?? ""}>
|
||||
<p className="text-sm font-bold uppercase text-settings-sidebar-type-secondary mb-2">
|
||||
{props.title}
|
||||
</p>
|
||||
|
@ -33,7 +33,7 @@ export function ColorOption(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function CaptionSetting(props: {
|
||||
export function CaptionSetting(props: {
|
||||
textTransformer?: (s: string) => string;
|
||||
value: number;
|
||||
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 }) {
|
||||
const router = useOverlayRouter(id);
|
||||
|
@ -46,10 +46,10 @@ export function CaptionCue({
|
||||
|
||||
return (
|
||||
<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={{
|
||||
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)})`,
|
||||
}}
|
||||
>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import classNames from "classnames";
|
||||
import { useEffect } from "react";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
@ -5,8 +6,10 @@ import { getSessions } from "@/backend/accounts/sessions";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { AccountActionsPart } from "@/pages/settings/AccountActionsPart";
|
||||
import { AccountEditPart } from "@/pages/settings/AccountEditPart";
|
||||
import { CaptionsPart } from "@/pages/settings/CaptionsPart";
|
||||
import { DeviceListPart } from "@/pages/settings/DeviceListPart";
|
||||
import { RegisterCalloutPart } from "@/pages/settings/RegisterCalloutPart";
|
||||
import { SidebarPart } from "@/pages/settings/SidebarPart";
|
||||
@ -17,9 +20,16 @@ import { useThemeStore } from "@/stores/theme";
|
||||
import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||
|
||||
function SettingsLayout(props: { children: React.ReactNode }) {
|
||||
const { isMobile } = useIsMobile();
|
||||
|
||||
return (
|
||||
<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 />
|
||||
<div className="space-y-16">{props.children}</div>
|
||||
</div>
|
||||
@ -59,15 +69,22 @@ export function SettingsPage() {
|
||||
return (
|
||||
<SubPageLayout>
|
||||
<SettingsLayout>
|
||||
<Heading1 border className="!mb-0">
|
||||
Account
|
||||
</Heading1>
|
||||
{user.account ? (
|
||||
<AccountSettings account={user.account} />
|
||||
) : (
|
||||
<RegisterCalloutPart />
|
||||
)}
|
||||
<ThemePart active={activeTheme} setTheme={setTheme} />
|
||||
<div id="settings-account">
|
||||
<Heading1 border className="!mb-0">
|
||||
Account
|
||||
</Heading1>
|
||||
{user.account ? (
|
||||
<AccountSettings account={user.account} />
|
||||
) : (
|
||||
<RegisterCalloutPart />
|
||||
)}
|
||||
</div>
|
||||
<div id="settings-appearance">
|
||||
<ThemePart active={activeTheme} setTheme={setTheme} />
|
||||
</div>
|
||||
<div id="settings-captions">
|
||||
<CaptionsPart />
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</SubPageLayout>
|
||||
);
|
||||
|
@ -25,7 +25,7 @@ export function AccountActionsPart() {
|
||||
<Heading2 border>Actions</Heading2>
|
||||
<SolidSettingsCard
|
||||
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>
|
||||
<Heading3>Delete account</Heading3>
|
||||
@ -34,7 +34,7 @@ export function AccountActionsPart() {
|
||||
can be recovered.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end items-center">
|
||||
<div className="flex justify-start lg:justify-end items-center">
|
||||
<Button
|
||||
theme="danger"
|
||||
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 { Icons } from "@/components/Icon";
|
||||
import { SidebarLink, SidebarSection } from "@/components/layout/Sidebar";
|
||||
import { Divider } from "@/components/utils/Divider";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { conf } from "@/setup/config";
|
||||
|
||||
const percentageVisible = 10;
|
||||
|
||||
export function SidebarPart() {
|
||||
const { isMobile } = useIsMobile();
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const hostname = location.hostname;
|
||||
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 (
|
||||
<div>
|
||||
<Sticky
|
||||
enabled
|
||||
enabled={!isMobile}
|
||||
top={10 * rem} // 10rem
|
||||
className="text-settings-sidebar-type-inactive"
|
||||
>
|
||||
<SidebarSection title="Settings">
|
||||
<SidebarLink icon={Icons.WAND}>A war in my name!</SidebarLink>
|
||||
<SidebarLink active icon={Icons.COMPRESS}>
|
||||
TANSTAAFL
|
||||
</SidebarLink>
|
||||
<SidebarLink icon={Icons.AIRPLAY}>We all float down here</SidebarLink>
|
||||
<SidebarLink icon={Icons.BOOKMARK}>My skin is not my own</SidebarLink>
|
||||
</SidebarSection>
|
||||
<Divider />
|
||||
<div className="hidden lg:block">
|
||||
<SidebarSection title="Settings">
|
||||
{settingLinks.map((v) => (
|
||||
<SidebarLink
|
||||
icon={v.icon}
|
||||
active={v.id === activeLink}
|
||||
onClick={() => scrollTo(v.id)}
|
||||
>
|
||||
{v.text}
|
||||
</SidebarLink>
|
||||
))}
|
||||
</SidebarSection>
|
||||
<Divider />
|
||||
</div>
|
||||
<SidebarSection title="App information">
|
||||
<div className="flex justify-between items-center space-x-3">
|
||||
<span>Version</span>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Heading2 } from "@/components/utils/Text";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
|
||||
const availableThemes = [
|
||||
{
|
||||
@ -117,7 +117,7 @@ export function ThemePart(props: {
|
||||
}) {
|
||||
return (
|
||||
<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]">
|
||||
{/* default theme */}
|
||||
<ThemePreview
|
||||
|
Loading…
Reference in New Issue
Block a user