mirror of
https://github.com/ilyhalight/voice-over-translation.git
synced 2026-10-19 07:43:33 +00:00
refactor: rewrited appearance and misc section to solid, added Slider and Select components
This commit is contained in:
parent
166a66ccd0
commit
1b12fa65be
46 changed files with 1284 additions and 287 deletions
12
changelog.md
12
changelog.md
|
|
@ -1,10 +1,16 @@
|
|||
# 1.12.0 (WIP)
|
||||
|
||||
- Переработан дизайн блока настроек "Мой аккаунт"
|
||||
- Переработан дизайн блока "О расширении"
|
||||
- Все `Checkbox` в настройках были заменены на `Switch`
|
||||
Это обновление, в большинстве своем, посвящено технической части проекта. Основное внимание уделено полной миграции интерфейса на SolidJS и обновлению его дизайна. Эти изменения упрощают дальнейшее развитие UI, делают его более единообразным и устраняют ряд небольших технических проблем.
|
||||
|
||||
- Проведен редизайн некоторых элементов интерфейса расширения
|
||||
- Переработан дизайн блока настроек "Мой аккаунт"
|
||||
- Переработан дизайн блока "О расширении"
|
||||
- Переработан дизайн слайдеров
|
||||
- Переработан дизайн Select элементов
|
||||
- `Checkbox` были заменены на `Switch` элементы
|
||||
- Весь UI расширения переведен на SolidJS
|
||||
- Исправлена ошибка из-за которой Debug логи попадали в релизную сборку расширения
|
||||
- Другие небольшие технические исправления
|
||||
|
||||
# 1.11.8
|
||||
|
||||
|
|
|
|||
|
|
@ -13,5 +13,9 @@
|
|||
&__value {
|
||||
text-align: end;
|
||||
color: rgba(var(--vot-primary-rgb, 33, 150, 243), 0.9);
|
||||
|
||||
&_detail {
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.68);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@ export function AboutSection(props: AccountSectionProps): JSX.Element {
|
|||
</AboutItem>
|
||||
<AboutItem label={localizationProvider.get("VOTLocaleHash")}>
|
||||
{localeHashValue()}
|
||||
<br />({localizationProvider.get("VOTUpdatedAt")} {localeUpdatedAt})
|
||||
<br />
|
||||
<vot-block class="vot-about-item__value_detail">
|
||||
({localizationProvider.get("VOTUpdatedAt")} {localeUpdatedAt})
|
||||
</vot-block>
|
||||
</AboutItem>
|
||||
<OutlinedButton
|
||||
onClick={async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
.vot-account {
|
||||
&-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.vot-account-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
|
|
|||
1
src/components/Button/DetailsButton.scss
Normal file
1
src/components/Button/DetailsButton.scss
Normal file
|
|
@ -0,0 +1 @@
|
|||
@use "../../styles/components/details.scss";
|
||||
22
src/components/Button/DetailsButton.stories.ts
Normal file
22
src/components/Button/DetailsButton.stories.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from "storybook-solidjs-vite";
|
||||
|
||||
import { DetailsButton } from "./DetailsButton";
|
||||
|
||||
const meta = {
|
||||
component: DetailsButton,
|
||||
} satisfies Meta<typeof DetailsButton>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const DetailsButtonDefault: Story = {
|
||||
args: {
|
||||
children: "Details",
|
||||
},
|
||||
};
|
||||
export const DetailsButtonDisabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
children: "Details",
|
||||
},
|
||||
};
|
||||
22
src/components/Button/DetailsButton.tsx
Normal file
22
src/components/Button/DetailsButton.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { type JSX, splitProps } from "solid-js";
|
||||
|
||||
import "./DetailsButton.scss";
|
||||
import { RawButton, type RawButtonProps } from "../Button/RawButton";
|
||||
import { ChevronIcon } from "../Icons/ChevronIcon";
|
||||
|
||||
export type DetailsButtonProps = Omit<RawButtonProps, "class"> & {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export function DetailsButton(props: DetailsButtonProps): JSX.Element {
|
||||
const [local, buttonProps] = splitProps(props, ["children"]);
|
||||
|
||||
return (
|
||||
<RawButton {...buttonProps} class="vot-details">
|
||||
<vot-block>{local.children}</vot-block>
|
||||
<vot-block class="vot-details-arrow-icon">
|
||||
<ChevronIcon />
|
||||
</vot-block>
|
||||
</RawButton>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { JSX } from "solid-js";
|
||||
import { type JSX, splitProps } from "solid-js";
|
||||
|
||||
import "./IconButton.scss";
|
||||
import { RawButton, type RawButtonProps } from "./RawButton";
|
||||
|
|
@ -8,5 +8,16 @@ export type IconButtonProps = Omit<RawButtonProps, "class" | "ariaLabel"> & {
|
|||
};
|
||||
|
||||
export function IconButton(props: IconButtonProps): JSX.Element {
|
||||
return <RawButton {...props} class="vot-icon-button" />;
|
||||
const [local, rest] = splitProps(props, ["ariaLabel", "buttonProps"]);
|
||||
|
||||
return (
|
||||
<RawButton
|
||||
{...rest}
|
||||
class="vot-icon-button"
|
||||
buttonProps={{
|
||||
"aria-label": local.ariaLabel,
|
||||
...local.buttonProps,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import { type JSX, mergeProps } from "solid-js";
|
||||
import type { OnClickEvent } from "../../types/components/shared";
|
||||
import type {
|
||||
DataAttributes,
|
||||
OnClickEvent,
|
||||
} from "../../types/components/shared";
|
||||
import { isKeyboardActivation } from "../../ui/components/componentShared";
|
||||
|
||||
export type RawButtonProps = {
|
||||
class: string;
|
||||
ariaLabel?: string;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
ref?: (element: HTMLElement) => void;
|
||||
children: JSX.Element;
|
||||
onClick?: (event: OnClickEvent) => void;
|
||||
buttonProps?: JSX.HTMLAttributes<HTMLElement> & DataAttributes;
|
||||
};
|
||||
|
||||
export function RawButton(props: RawButtonProps): JSX.Element {
|
||||
|
|
@ -22,7 +25,6 @@ export function RawButton(props: RawButtonProps): JSX.Element {
|
|||
class={finalProps.class}
|
||||
role="button"
|
||||
tabIndex={tabIndex()}
|
||||
aria-label={finalProps.ariaLabel}
|
||||
aria-disabled={finalProps.disabled ? "true" : undefined}
|
||||
hidden={finalProps.hidden}
|
||||
onClick={(event) => {
|
||||
|
|
@ -46,6 +48,7 @@ export function RawButton(props: RawButtonProps): JSX.Element {
|
|||
|
||||
event.currentTarget.click();
|
||||
}}
|
||||
{...finalProps.buttonProps}
|
||||
>
|
||||
{finalProps.children}
|
||||
</vot-block>
|
||||
|
|
|
|||
97
src/components/Control/Select.scss
Normal file
97
src/components/Control/Select.scss
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
.vot-select_new {
|
||||
--vot-select-inner-bg: rgb(var(--vot-secondary-rgb, 43, 44, 48));
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
&-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-outer {
|
||||
cursor: pointer;
|
||||
|
||||
width: 120px;
|
||||
max-width: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5em;
|
||||
border-radius: 0.75em;
|
||||
border: 1px solid var(--vot-border-color);
|
||||
color: rgba(var(--vot-onsurface-rgb), 0.87);
|
||||
transition: border-color var(--vot-duration-medium)
|
||||
var(--vot-easing-standard);
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
height: 1.5em;
|
||||
font-size: 0.8em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
display: flex;
|
||||
font-size: 1.5em;
|
||||
fill: currentColor;
|
||||
transition: transform var(--vot-duration-medium)
|
||||
var(--vot-easing-standard);
|
||||
}
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.38;
|
||||
}
|
||||
|
||||
&:not([aria-disabled="true"]):hover {
|
||||
border-color: var(--vot-border-color-hover);
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] .vot-select_new-outer__arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&-inner {
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em;
|
||||
border-radius: 0.75em;
|
||||
background: var(--vot-select-inner-bg);
|
||||
top: calc(100% + 0.5em);
|
||||
right: 0;
|
||||
max-height: min(85vh, 500px);
|
||||
overflow: scroll;
|
||||
|
||||
&__option {
|
||||
cursor: pointer;
|
||||
padding: 0.5em 0.75em;
|
||||
border-radius: 0.5em;
|
||||
transition: background var(--vot-duration-medium)
|
||||
var(--vot-easing-standard);
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--vot-onsurface-rgb), 0.14);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
color: rgb(var(--vot-primary-rgb, 33, 150, 243));
|
||||
background-color: rgba(var(--vot-primary-rgb, 33, 150, 243), 0.2);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(
|
||||
var(--vot-primary-rgb, 33, 150, 243),
|
||||
0.1
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/components/Control/Select.stories.tsx
Normal file
40
src/components/Control/Select.stories.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { Meta, StoryObj } from "storybook-solidjs-vite";
|
||||
|
||||
import { Select, type SelectOption } from "./Select";
|
||||
|
||||
const meta = {
|
||||
component: Select,
|
||||
} satisfies Meta<typeof Select>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const selectOptions: SelectOption[] = [
|
||||
{
|
||||
label: "Option 1",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: "Option 2",
|
||||
value: "two",
|
||||
},
|
||||
{
|
||||
label: "Option 3",
|
||||
value: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export const SelectDefault: Story = {
|
||||
args: {
|
||||
title: "Select something",
|
||||
options: selectOptions,
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectDisabled: Story = {
|
||||
args: {
|
||||
title: "Select something",
|
||||
disabled: true,
|
||||
options: selectOptions,
|
||||
},
|
||||
};
|
||||
146
src/components/Control/Select.tsx
Normal file
146
src/components/Control/Select.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import {
|
||||
createSignal,
|
||||
createUniqueId,
|
||||
For,
|
||||
type JSX,
|
||||
mergeProps,
|
||||
onCleanup,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { effect } from "solid-js/web";
|
||||
|
||||
import "./Select.scss";
|
||||
import { RawButton } from "../Button/RawButton";
|
||||
import { ChevronIcon } from "../Icons/ChevronIcon";
|
||||
|
||||
export type SelectOption = {
|
||||
value: string | number | boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type SelectProps = {
|
||||
title: string;
|
||||
options: SelectOption[];
|
||||
children?: JSX.Element;
|
||||
selectedValue?: number;
|
||||
disabled?: boolean;
|
||||
isOpen?: boolean;
|
||||
onSelect?: (option: SelectOption) => void;
|
||||
ref?: (element: HTMLElement) => void;
|
||||
};
|
||||
|
||||
export function Select(props: SelectProps): JSX.Element {
|
||||
const finalProps = mergeProps(
|
||||
{
|
||||
disabled: false,
|
||||
isOpen: false,
|
||||
selectedValue: -1,
|
||||
},
|
||||
props,
|
||||
);
|
||||
|
||||
let outerRef!: HTMLElement;
|
||||
let innerRef!: HTMLElement;
|
||||
|
||||
const selectId = createUniqueId();
|
||||
const [disabled, setDisabled] = createSignal(finalProps.disabled);
|
||||
const [selectedValue, setSelectedValue] = createSignal<number>(
|
||||
finalProps.selectedValue,
|
||||
);
|
||||
|
||||
const visibleTitle = () =>
|
||||
finalProps.options[selectedValue()]?.label || finalProps.title;
|
||||
const [isOpen, setIsOpen] = createSignal(finalProps.isOpen);
|
||||
|
||||
effect(() => {
|
||||
setDisabled(finalProps.disabled);
|
||||
setIsOpen(finalProps.isOpen);
|
||||
setSelectedValue(finalProps.selectedValue);
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
const path = event.composedPath();
|
||||
if (!path.includes(innerRef) && !path.includes(outerRef)) {
|
||||
console.log("click outside select, closing", path);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointerdown", handlePointerDown);
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("pointerdown", handlePointerDown);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<vot-block
|
||||
ref={finalProps.ref}
|
||||
class="vot-select_new"
|
||||
aria-disabled={disabled()}
|
||||
>
|
||||
<Show when={finalProps.children}>
|
||||
<vot-block class="vot-select_new-label">
|
||||
{finalProps.children}
|
||||
</vot-block>
|
||||
</Show>
|
||||
<RawButton
|
||||
ref={(el) => (outerRef = el)}
|
||||
class="vot-select_new-outer"
|
||||
buttonProps={{
|
||||
"aria-haspopup": "menu",
|
||||
"aria-controls": selectId,
|
||||
"aria-expanded": isOpen(),
|
||||
}}
|
||||
disabled={disabled()}
|
||||
onClick={() => {
|
||||
console.log("current state", isOpen());
|
||||
setIsOpen(!isOpen());
|
||||
console.log("new state", isOpen());
|
||||
}}
|
||||
>
|
||||
<vot-block class="vot-select_new-outer__title">
|
||||
{visibleTitle()}
|
||||
</vot-block>
|
||||
<vot-block class="vot-select_new-outer__arrow">
|
||||
<ChevronIcon />
|
||||
</vot-block>
|
||||
</RawButton>
|
||||
<vot-block
|
||||
class="vot-select_new-inner"
|
||||
ref={innerRef}
|
||||
id={selectId}
|
||||
role="menu"
|
||||
hidden={!isOpen()}
|
||||
>
|
||||
<For each={finalProps.options}>
|
||||
{(option, idx) => (
|
||||
<RawButton
|
||||
class="vot-select_new-inner__option"
|
||||
disabled={disabled()}
|
||||
buttonProps={{
|
||||
role: "menuitem",
|
||||
classList: {
|
||||
"vot-select_new-inner__option--selected":
|
||||
idx() === selectedValue(),
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedValue(idx());
|
||||
setIsOpen(false);
|
||||
finalProps.onSelect?.(option);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</RawButton>
|
||||
)}
|
||||
</For>
|
||||
</vot-block>
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
64
src/components/Control/Slider.scss
Normal file
64
src/components/Control/Slider.scss
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
.vot-slider_new {
|
||||
--vot-slider-track-bg: rgba(var(--vot-onsurface-rgb, 227, 227, 227), 0.15);
|
||||
--vot-slider-track-progress-bg: rgb(var(--vot-primary-rgb, 33, 150, 243));
|
||||
--vot-slider-size: 1em;
|
||||
--vot-slider-border-radius: 0.5em;
|
||||
--vot-slider-handle-bg: rgb(var(--vot-primary-rgb, 139, 180, 245));
|
||||
--vot-slider-handle-size: 44px;
|
||||
--vot-slider-transition: var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: var(--vot-slider-handle-size);
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
user-select: none;
|
||||
opacity: 0.5;
|
||||
.vot-slider__control {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__control {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__track {
|
||||
background: var(--vot-slider-track-bg);
|
||||
width: 100%;
|
||||
height: var(--vot-slider-size);
|
||||
border-radius: var(--vot-slider-border-radius);
|
||||
pointer-events: none;
|
||||
&-progress {
|
||||
background: var(--vot-slider-track-progress-bg);
|
||||
position: absolute;
|
||||
width: calc(100% * var(--vot-progress, 0));
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
will-change: width;
|
||||
transition: width var(--vot-slider-transition);
|
||||
}
|
||||
}
|
||||
|
||||
&__handle {
|
||||
position: absolute;
|
||||
left: calc(100% * var(--vot-progress, 0));
|
||||
width: 4px;
|
||||
height: var(--vot-slider-handle-size);
|
||||
background: var(--vot-slider-handle-bg);
|
||||
outline: 4px solid var(--vot-slider-track-bg);
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
will-change: transform;
|
||||
transition:
|
||||
transform var(--vot-slider-transition),
|
||||
left var(--vot-slider-transition);
|
||||
transform: translateX(calc(-100% * var(--vot-progress, 0)));
|
||||
}
|
||||
}
|
||||
26
src/components/Control/Slider.stories.tsx
Normal file
26
src/components/Control/Slider.stories.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { Meta, StoryObj } from "storybook-solidjs-vite";
|
||||
|
||||
import { Slider } from "./Slider";
|
||||
|
||||
const meta = {
|
||||
component: Slider,
|
||||
} satisfies Meta<typeof Slider>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const SliderDefault: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const SliderDiffStep: Story = {
|
||||
args: {
|
||||
step: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const SliderDisabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
74
src/components/Control/Slider.tsx
Normal file
74
src/components/Control/Slider.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { createSignal, type JSX, mergeProps } from "solid-js";
|
||||
import "./Slider.scss";
|
||||
import { effect } from "solid-js/web";
|
||||
import { clampNumber } from "../../utils/number";
|
||||
|
||||
export type SliderProps = {
|
||||
min?: number;
|
||||
max?: number;
|
||||
value?: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
onInput?: (value: number) => void;
|
||||
ref?: (element: HTMLElement) => void;
|
||||
};
|
||||
|
||||
export function Slider(props: SliderProps): JSX.Element {
|
||||
const finalProps = mergeProps(
|
||||
{
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: 50,
|
||||
step: 1,
|
||||
disabled: false,
|
||||
},
|
||||
props,
|
||||
);
|
||||
|
||||
const [value, setValue] = createSignal(finalProps.value);
|
||||
const [disabled, setDisabled] = createSignal(finalProps.disabled);
|
||||
const [min, setMin] = createSignal(finalProps.min);
|
||||
const [max, setMax] = createSignal(finalProps.max);
|
||||
const [step, setStep] = createSignal(finalProps.step);
|
||||
|
||||
const progress = () => {
|
||||
const range = max() - min();
|
||||
const raw = range <= 0 ? 0 : (value() - min()) / range;
|
||||
return clampNumber(raw, 0, 1);
|
||||
};
|
||||
|
||||
effect(() => {
|
||||
setValue(finalProps.value);
|
||||
setDisabled(finalProps.disabled);
|
||||
setMin(finalProps.min);
|
||||
setMax(finalProps.max);
|
||||
setStep(finalProps.step);
|
||||
});
|
||||
|
||||
return (
|
||||
<vot-block
|
||||
ref={finalProps.ref}
|
||||
class="vot-slider_new"
|
||||
style={{ "--vot-progress": progress() }}
|
||||
aria-disabled={disabled()}
|
||||
>
|
||||
<input
|
||||
class="vot-slider_new__control"
|
||||
type="range"
|
||||
min={min()}
|
||||
max={max()}
|
||||
value={value()}
|
||||
step={step()}
|
||||
disabled={disabled()}
|
||||
oninput={(e) => {
|
||||
const newValue = e.target.valueAsNumber;
|
||||
setValue(newValue);
|
||||
finalProps.onInput?.(newValue);
|
||||
}}
|
||||
/>
|
||||
<vot-block class="vot-slider_new__track" />
|
||||
<vot-block class="vot-slider_new__track vot-slider_new__track-progress" />
|
||||
<vot-block class="vot-slider_new__handle" />
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
24
src/components/Control/SliderLabel.scss
Normal file
24
src/components/Control/SliderLabel.scss
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
.vot-slider_new-label {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: baseline;
|
||||
gap: 0.75em;
|
||||
|
||||
&__text {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__value {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
user-select: none;
|
||||
color: rgba(var(--vot-onsurface-rgb, 227, 227, 227), 0.5);
|
||||
}
|
||||
}
|
||||
33
src/components/Control/SliderLabel.stories.tsx
Normal file
33
src/components/Control/SliderLabel.stories.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { Meta, StoryObj } from "storybook-solidjs-vite";
|
||||
|
||||
import { SliderLabel } from "./SliderLabel";
|
||||
|
||||
const meta = {
|
||||
component: SliderLabel,
|
||||
} satisfies Meta<typeof SliderLabel>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const SliderLabelDefault: Story = {
|
||||
args: {
|
||||
children: "Auto Hide Button Delay",
|
||||
value: "50",
|
||||
},
|
||||
};
|
||||
|
||||
export const SliderLabelDisabled: Story = {
|
||||
args: {
|
||||
children: "Auto Hide Button Delay",
|
||||
value: "50",
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const SliderLabelWithSuperLongTitle: Story = {
|
||||
args: {
|
||||
children:
|
||||
"lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
value: "50",
|
||||
},
|
||||
};
|
||||
35
src/components/Control/SliderLabel.tsx
Normal file
35
src/components/Control/SliderLabel.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { type JSX, mergeProps } from "solid-js";
|
||||
|
||||
import "./SliderLabel.scss";
|
||||
|
||||
export type SliderLabelProps = {
|
||||
children: JSX.Element;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
ref?: (element: HTMLElement) => void;
|
||||
};
|
||||
|
||||
export function SliderLabel(props: SliderLabelProps): JSX.Element {
|
||||
const finalProps = mergeProps(
|
||||
{
|
||||
disabled: false,
|
||||
value: "0",
|
||||
},
|
||||
props,
|
||||
);
|
||||
|
||||
return (
|
||||
<vot-block
|
||||
ref={finalProps.ref}
|
||||
class="vot-slider_new-label"
|
||||
aria-disabled={finalProps.disabled}
|
||||
>
|
||||
<vot-block class="vot-slider_new-label__text">
|
||||
{finalProps.children}
|
||||
</vot-block>
|
||||
<vot-block class="vot-slider_new-label__value">
|
||||
{finalProps.value}
|
||||
</vot-block>
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
5
src/components/Control/SliderWrapper.scss
Normal file
5
src/components/Control/SliderWrapper.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.vot-slider_new-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
}
|
||||
44
src/components/Control/SliderWrapper.stories.tsx
Normal file
44
src/components/Control/SliderWrapper.stories.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { createSignal } from "solid-js";
|
||||
import type { Meta, StoryObj } from "storybook-solidjs-vite";
|
||||
import { Slider } from "./Slider";
|
||||
import { SliderLabel } from "./SliderLabel";
|
||||
import { SliderWrapper } from "./SliderWrapper";
|
||||
|
||||
const meta = {
|
||||
component: SliderWrapper,
|
||||
} satisfies Meta<typeof SliderWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const [value, setValue] = createSignal(50);
|
||||
|
||||
const SliderWrapperContent = (
|
||||
disabled: boolean,
|
||||
title = "Auto Hide Button Delay",
|
||||
) => (
|
||||
<>
|
||||
<SliderLabel value={`${value()}%`} disabled={disabled}>
|
||||
{title}
|
||||
</SliderLabel>
|
||||
<Slider
|
||||
value={value()}
|
||||
disabled={disabled}
|
||||
onInput={(val) => {
|
||||
setValue(val);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
export const SliderWrapperDefault: Story = {
|
||||
args: {
|
||||
children: SliderWrapperContent(false),
|
||||
},
|
||||
};
|
||||
|
||||
export const SliderWrapperWithDisabled: Story = {
|
||||
args: {
|
||||
children: SliderWrapperContent(true),
|
||||
},
|
||||
};
|
||||
16
src/components/Control/SliderWrapper.tsx
Normal file
16
src/components/Control/SliderWrapper.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { JSX } from "solid-js";
|
||||
|
||||
import "./SliderWrapper.scss";
|
||||
|
||||
export type SliderWrapperProps = {
|
||||
children: JSX.Element;
|
||||
ref?: (element: HTMLElement) => void;
|
||||
};
|
||||
|
||||
export function SliderWrapper(props: SliderWrapperProps): JSX.Element {
|
||||
return (
|
||||
<vot-block ref={props.ref} class="vot-slider_new-wrapper">
|
||||
{props.children}
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
@use "../../styles/mixins" as shared;
|
||||
|
||||
.vot-switch {
|
||||
--vot-switch-outline: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.6);
|
||||
--vot-switch-primary: rgb(var(--vot-primary-rgb, 33, 150, 243));
|
||||
--vot-switch-onprimary: rgb(var(--vot-onprimary-rgb, 32, 33, 36));
|
||||
--vot-switch-track: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.12);
|
||||
|
||||
--vot-switch-timing: cubic-bezier(0.2, 0, 0, 1);
|
||||
--vot-switch-duration: var(--vot-duration-medium, 200ms);
|
||||
--vot-switch-label-offset: 24px;
|
||||
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -25,9 +27,13 @@
|
|||
position: relative;
|
||||
touch-action: manipulation;
|
||||
vertical-align: middle;
|
||||
|
||||
&[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&_sub {
|
||||
padding-left: var(--vot-switch-label-offset) !important;
|
||||
}
|
||||
|
||||
&-control {
|
||||
|
|
@ -129,7 +135,7 @@
|
|||
position: relative;
|
||||
transition-duration: var(--vot-switch-duration);
|
||||
transition-property: background-color, border-color;
|
||||
transition-timing-function: var(--vot-switch-timing);
|
||||
transition-timing-function: var(--vot-easing-standard);
|
||||
width: 52px;
|
||||
|
||||
&[data-checked="true"] {
|
||||
|
|
@ -156,7 +162,7 @@
|
|||
transform: translate(0, -50%);
|
||||
transition-duration: var(--vot-switch-duration);
|
||||
transition-property: background-color, box-shadow, height, transform, width;
|
||||
transition-timing-function: var(--vot-switch-timing);
|
||||
transition-timing-function: var(--vot-easing-standard);
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,19 @@ export const SwitchCheckedDisabled: Story = {
|
|||
checked: true,
|
||||
},
|
||||
};
|
||||
export const SwitchHidden: Story = {
|
||||
args: {
|
||||
hidden: true,
|
||||
heading: "hidden",
|
||||
description: "you shouldn't see this",
|
||||
},
|
||||
};
|
||||
export const SwitchSub: Story = {
|
||||
args: {
|
||||
isSubSwitch: true,
|
||||
heading: "Translate videos automatically",
|
||||
},
|
||||
};
|
||||
|
||||
export const SwitchWithHeading: Story = {
|
||||
args: {
|
||||
|
|
|
|||
|
|
@ -6,25 +6,44 @@ import {
|
|||
Show,
|
||||
} from "solid-js";
|
||||
import "./Switch.scss";
|
||||
import { effect } from "solid-js/web";
|
||||
|
||||
export type SwitchProps = {
|
||||
checked?: boolean;
|
||||
description?: JSX.Element;
|
||||
disabled?: boolean;
|
||||
heading?: JSX.Element;
|
||||
isSubSwitch?: boolean;
|
||||
hidden?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
ref?: (element: HTMLLabelElement) => void;
|
||||
};
|
||||
|
||||
export function Switch(props: SwitchProps): JSX.Element {
|
||||
const finalProps = mergeProps({ checked: false, disabled: false }, props);
|
||||
const finalProps = mergeProps(
|
||||
{ checked: false, disabled: false, isSubSwitch: false },
|
||||
props,
|
||||
);
|
||||
const textId = createUniqueId();
|
||||
|
||||
const [checked, setChecked] = createSignal(finalProps.checked);
|
||||
const [disabled] = createSignal(finalProps.disabled);
|
||||
const [disabled, setDisabled] = createSignal(finalProps.disabled);
|
||||
|
||||
effect(() => {
|
||||
setChecked(finalProps.checked);
|
||||
setDisabled(finalProps.disabled);
|
||||
});
|
||||
|
||||
return (
|
||||
<label ref={finalProps.ref} class="vot-switch" data-disabled={disabled()}>
|
||||
<label
|
||||
ref={finalProps.ref}
|
||||
class="vot-switch"
|
||||
classList={{
|
||||
"vot-switch_sub": finalProps.isSubSwitch,
|
||||
}}
|
||||
hidden={finalProps.hidden}
|
||||
data-disabled={disabled()}
|
||||
>
|
||||
<input
|
||||
class="vot-switch-control"
|
||||
aria-checked={checked()}
|
||||
|
|
|
|||
9
src/components/Icons/ChevronIcon.tsx
Normal file
9
src/components/Icons/ChevronIcon.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type { JSX } from "solid-js";
|
||||
|
||||
export function ChevronIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path d="M12 14.975q-.2 0-.375-.062T11.3 14.7l-4.6-4.6q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l3.9 3.9l3.9-3.9q.275-.275.7-.275t.7.275q.275.275.275.7t-.275.7l-4.6 4.6q-.15.15-.325.213t-.375.062Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
108
src/components/Settings/SettingsAppearanceSection.tsx
Normal file
108
src/components/Settings/SettingsAppearanceSection.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { createSignal, type JSX, mergeProps } from "solid-js";
|
||||
import { DEFAULT_AUTO_HIDE_DELAY } from "../../config/config";
|
||||
import { localizationProvider } from "../../localization/localizationProvider";
|
||||
import { settings } from "../../stores/settings";
|
||||
import { positions } from "../../types/components/votButton";
|
||||
import type { Phrase } from "../../types/localization";
|
||||
import { isPiPAvailable } from "../../utils/utils";
|
||||
import { Select, type SelectOption } from "../Control/Select";
|
||||
import { Slider } from "../Control/Slider";
|
||||
import { SliderLabel } from "../Control/SliderLabel";
|
||||
import { SliderWrapper } from "../Control/SliderWrapper";
|
||||
import { Switch } from "../Control/Switch";
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
|
||||
export type SettingsAppearanceSectionProps = {
|
||||
ref?: (element: HTMLElement) => void;
|
||||
onShowPiPButtonChange?: (checked: boolean) => void;
|
||||
onAutoHideButtonDelayInput?: (delay: number) => void;
|
||||
onButtonPositionSelect?: (option: SelectOption) => void;
|
||||
onLangSelect?: (option: SelectOption) => void;
|
||||
};
|
||||
|
||||
const MAX_AUTO_HIDE_BUTTON_DELAY = 3000;
|
||||
const MIN_AUTO_HIDE_BUTTON_DELAY = 100;
|
||||
const STEP_AUTO_HIDE_BUTTON_DELAY = 100;
|
||||
|
||||
export function SettingsAppearanceSection(
|
||||
props: SettingsAppearanceSectionProps,
|
||||
): JSX.Element {
|
||||
const finalProps = mergeProps(props);
|
||||
|
||||
const [autoHideButtonDelay, setAutoHideButtonDelay] = createSignal(
|
||||
DEFAULT_AUTO_HIDE_DELAY,
|
||||
);
|
||||
const autoHideButtonDelaySecs = () =>
|
||||
Math.round(autoHideButtonDelay() / STEP_AUTO_HIDE_BUTTON_DELAY) / 10;
|
||||
const autoHideButtonDelayValueText = () =>
|
||||
`${autoHideButtonDelaySecs()} ${localizationProvider.get("secs")}`;
|
||||
|
||||
const buttonPositionOptions: SelectOption[] = positions.map((position) => ({
|
||||
label: localizationProvider.get(`position.${position}`),
|
||||
value: position,
|
||||
}));
|
||||
const selectedButtonPositionIndex = () =>
|
||||
buttonPositionOptions.findIndex(
|
||||
(option) => option.value === settings.buttonPos,
|
||||
);
|
||||
|
||||
const langsOptions: SelectOption[] = localizationProvider
|
||||
.getAvailableLangs()
|
||||
.map((lang) => {
|
||||
const phrase = `langs.${lang}` satisfies Phrase;
|
||||
const label = localizationProvider.get(phrase);
|
||||
return {
|
||||
label: label === phrase ? lang.toUpperCase() : label,
|
||||
value: lang,
|
||||
};
|
||||
});
|
||||
const selectedLangIndex = () =>
|
||||
langsOptions.findIndex(
|
||||
(option) => option.value === localizationProvider.langOverride,
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
ref={finalProps.ref}
|
||||
title={localizationProvider.get("appearance")}
|
||||
>
|
||||
<Switch
|
||||
heading={localizationProvider.get("VOTShowPiPButton")}
|
||||
checked={settings.showPiPButton}
|
||||
hidden={!isPiPAvailable()}
|
||||
onChange={finalProps.onShowPiPButtonChange}
|
||||
/>
|
||||
<SliderWrapper>
|
||||
<SliderLabel value={autoHideButtonDelayValueText()}>
|
||||
{localizationProvider.get("autoHideButtonDelay")}
|
||||
</SliderLabel>
|
||||
<Slider
|
||||
min={MIN_AUTO_HIDE_BUTTON_DELAY}
|
||||
max={MAX_AUTO_HIDE_BUTTON_DELAY}
|
||||
step={STEP_AUTO_HIDE_BUTTON_DELAY}
|
||||
value={autoHideButtonDelay()}
|
||||
onInput={(val) => {
|
||||
setAutoHideButtonDelay(val);
|
||||
finalProps.onAutoHideButtonDelayInput?.(val);
|
||||
}}
|
||||
/>
|
||||
</SliderWrapper>
|
||||
<Select
|
||||
title={localizationProvider.get("buttonPosition")}
|
||||
options={buttonPositionOptions}
|
||||
selectedValue={selectedButtonPositionIndex()}
|
||||
onSelect={finalProps.onButtonPositionSelect}
|
||||
>
|
||||
{localizationProvider.get("buttonPosition")}
|
||||
</Select>
|
||||
<Select
|
||||
title={localizationProvider.get("VOTMenuLanguage")}
|
||||
options={langsOptions}
|
||||
selectedValue={selectedLangIndex()}
|
||||
onSelect={finalProps.onLangSelect}
|
||||
>
|
||||
{localizationProvider.get("VOTMenuLanguage")}
|
||||
</Select>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,25 @@
|
|||
import type { JSX } from "solid-js";
|
||||
import { localizationProvider } from "../../localization/localizationProvider";
|
||||
import { AboutSection } from "../About/AboutSection";
|
||||
import { AccountMenu } from "../Account/AccountMenu";
|
||||
import { SettingsAppearanceSection } from "./SettingsAppearanceSection";
|
||||
import { SettingsMiscSection } from "./SettingsMiscSection";
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
|
||||
export function SettingsContent(): JSX.Element {
|
||||
return (
|
||||
<vot-block style="max-width: 450px;display:block;margin:0 auto;">
|
||||
<AccountMenu />
|
||||
<AboutSection />
|
||||
<vot-block style="max-width: 450px;display:flex;flex-direction:column;gap: 16px;margin: 0 auto;">
|
||||
<SettingsSection
|
||||
title={localizationProvider.get("VOTMyAccount")}
|
||||
isOpen={true}
|
||||
>
|
||||
<AccountMenu />
|
||||
</SettingsSection>
|
||||
<SettingsAppearanceSection />
|
||||
<SettingsMiscSection />
|
||||
<SettingsSection title={localizationProvider.get("aboutExtension")}>
|
||||
<AboutSection />
|
||||
</SettingsSection>
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
64
src/components/Settings/SettingsMiscSection.tsx
Normal file
64
src/components/Settings/SettingsMiscSection.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { type JSX, mergeProps } from "solid-js";
|
||||
import { localizationProvider } from "../../localization/localizationProvider";
|
||||
import { settings } from "../../stores/settings";
|
||||
import { Switch } from "../Control/Switch";
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
|
||||
export type SettingsMiscSectionProps = {
|
||||
ref?: (element: HTMLElement) => void;
|
||||
onChangeTranslateAPIErrors?: (checked: boolean) => void;
|
||||
onChangeNewAudioPlayer?: (checked: boolean) => void;
|
||||
onChangeOnlyBypassMediaCSP?: (checked: boolean) => void;
|
||||
isAudioContextSupported?: boolean;
|
||||
needBypassCSP?: boolean;
|
||||
};
|
||||
|
||||
export function SettingsMiscSection(
|
||||
props: SettingsMiscSectionProps,
|
||||
): JSX.Element {
|
||||
const finalProps = mergeProps(
|
||||
{ isAudioContextSupported: false, needBypassCSP: false },
|
||||
props,
|
||||
);
|
||||
|
||||
const isWithoutAudioContext = () => !finalProps.isAudioContextSupported;
|
||||
const newAudioPlayer = () => settings.newAudioPlayer;
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
ref={finalProps.ref}
|
||||
title={localizationProvider.get("miscSettings")}
|
||||
>
|
||||
<Switch
|
||||
heading={localizationProvider.get("VOTTranslateAPIErrors")}
|
||||
hidden={localizationProvider.lang === "ru"}
|
||||
checked={settings.translateAPIErrors}
|
||||
onChange={finalProps.onChangeTranslateAPIErrors}
|
||||
/>
|
||||
<Switch
|
||||
heading={localizationProvider.get("VOTNewAudioPlayer")}
|
||||
description={
|
||||
isWithoutAudioContext()
|
||||
? localizationProvider.get("VOTNeedWebAudioAPI")
|
||||
: undefined
|
||||
}
|
||||
disabled={isWithoutAudioContext()}
|
||||
checked={newAudioPlayer()}
|
||||
onChange={finalProps.onChangeNewAudioPlayer}
|
||||
/>
|
||||
<Switch
|
||||
heading={localizationProvider.get("VOTOnlyBypassMediaCSP")}
|
||||
description={
|
||||
finalProps.needBypassCSP
|
||||
? localizationProvider.get("VOTMediaCSPEnabledOnSite")
|
||||
: undefined
|
||||
}
|
||||
checked={settings.onlyBypassMediaCSP}
|
||||
hidden={isWithoutAudioContext()}
|
||||
disabled={!newAudioPlayer()}
|
||||
isSubSwitch={true}
|
||||
onChange={finalProps.onChangeOnlyBypassMediaCSP}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
2
src/components/Settings/SettingsSection.scss
Normal file
2
src/components/Settings/SettingsSection.scss
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@use "../Button/DetailsButton.scss";
|
||||
@use "../../styles/components/settings.scss";
|
||||
31
src/components/Settings/SettingsSection.stories.tsx
Normal file
31
src/components/Settings/SettingsSection.stories.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Meta, StoryObj } from "storybook-solidjs-vite";
|
||||
|
||||
import { SettingsSection } from "./SettingsSection";
|
||||
|
||||
const meta = {
|
||||
component: SettingsSection,
|
||||
} satisfies Meta<typeof SettingsSection>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const SectionChildren = () => (
|
||||
<p>
|
||||
Hello <b>World</b>!
|
||||
</p>
|
||||
);
|
||||
|
||||
export const SettingsSectionDefault: Story = {
|
||||
args: {
|
||||
title: "Settings Section",
|
||||
children: SectionChildren(),
|
||||
},
|
||||
};
|
||||
|
||||
export const SettingsSectionDefaultOpened: Story = {
|
||||
args: {
|
||||
title: "Settings Section",
|
||||
children: SectionChildren(),
|
||||
isOpen: true,
|
||||
},
|
||||
};
|
||||
61
src/components/Settings/SettingsSection.tsx
Normal file
61
src/components/Settings/SettingsSection.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { createSignal, createUniqueId, type JSX, mergeProps } from "solid-js";
|
||||
import { effect } from "solid-js/web";
|
||||
|
||||
import "./SettingsSection.scss";
|
||||
|
||||
import { RawButton } from "../Button/RawButton";
|
||||
import { ChevronIcon } from "../Icons/ChevronIcon";
|
||||
|
||||
export type SettingsSectionProps = {
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
isOpen?: boolean;
|
||||
ref?: (element: HTMLElement) => void;
|
||||
};
|
||||
|
||||
export function SettingsSection(props: SettingsSectionProps): JSX.Element {
|
||||
const finalProps = mergeProps({ isOpen: false }, props);
|
||||
|
||||
const sectionId = createUniqueId();
|
||||
const headerId = `${sectionId}-header`;
|
||||
const contentId = `${sectionId}-content`;
|
||||
|
||||
const [isOpen, setIsOpen] = createSignal(finalProps.isOpen);
|
||||
const isHidden = () => !isOpen();
|
||||
|
||||
effect(() => {
|
||||
setIsOpen(finalProps.isOpen);
|
||||
});
|
||||
|
||||
return (
|
||||
<vot-block class="vot-settings-section" ref={finalProps.ref}>
|
||||
<RawButton
|
||||
class="vot-details vot-settings-section__header"
|
||||
buttonProps={{
|
||||
id: headerId,
|
||||
"aria-controls": contentId,
|
||||
"data-open": isOpen(),
|
||||
"aria-expanded": isOpen(),
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen());
|
||||
}}
|
||||
>
|
||||
<vot-block>{finalProps.title}</vot-block>
|
||||
<vot-block class="vot-details-arrow-icon">
|
||||
<ChevronIcon />
|
||||
</vot-block>
|
||||
</RawButton>
|
||||
|
||||
<vot-block
|
||||
class="vot-settings-section__content"
|
||||
id={contentId}
|
||||
role="region"
|
||||
aria-labelledby={headerId}
|
||||
hidden={isHidden()}
|
||||
>
|
||||
{finalProps.children}
|
||||
</vot-block>
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ export type TextfieldProps = {
|
|||
value?: string;
|
||||
multiline?: boolean;
|
||||
disabled?: boolean;
|
||||
ref?: (element: HTMLDivElement) => void;
|
||||
ref?: (element: HTMLElement) => void;
|
||||
inputRef?: (element: HTMLInputElement | HTMLTextAreaElement) => void;
|
||||
labelRef?: (element: HTMLSpanElement) => void;
|
||||
onInput?: (value: string) => void;
|
||||
|
|
@ -73,13 +73,13 @@ export function Textfield(props: TextfieldProps): JSX.Element {
|
|||
};
|
||||
|
||||
return (
|
||||
<div ref={finalProps.ref} class="vot-textfield">
|
||||
<vot-block ref={finalProps.ref} class="vot-textfield">
|
||||
<Show when={finalProps.multiline} fallback={<input {...common} />}>
|
||||
<textarea {...common} />
|
||||
</Show>
|
||||
<span ref={finalProps.labelRef} id={labelId}>
|
||||
{finalProps.labelText}
|
||||
</span>
|
||||
</div>
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
--vot-primary-rgb: 139, 180, 245;
|
||||
--vot-onprimary-rgb: 32, 33, 36;
|
||||
--vot-surface-rgb: 32, 33, 36;
|
||||
--vot-secondary-rgb: 43, 44, 48;
|
||||
--vot-onsurface-rgb: 227, 227, 227;
|
||||
|
||||
--vot-danger-rgb: 255, 99, 99;
|
||||
|
|
@ -39,10 +40,54 @@
|
|||
--vot-duration-fast: 120ms;
|
||||
--vot-duration-medium: 200ms;
|
||||
--vot-duration-slow: 320ms;
|
||||
--vot-easing-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--vot-easing-standard: cubic-bezier(0.2, 0, 0, 1);
|
||||
|
||||
--vot-focus-ring-color: rgba(var(--vot-primary-rgb, 139, 180, 245), 0.9);
|
||||
--vot-focus-ring: 0 0 0 2px var(--vot-focus-ring-color);
|
||||
--vot-focus-ring-offset: 0 0 0 4px
|
||||
rgba(var(--vot-surface-rgb, 32, 33, 36), 0.9);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Global baseline for injected UI
|
||||
// ------------------------------
|
||||
|
||||
// Avoid host-page box-sizing and typography resets from affecting our components.
|
||||
vot-block,
|
||||
vot-block * {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
// Enforce hidden attribute behavior inside the injected UI tree.
|
||||
// Keep opt-out classes for components that use custom hidden animations.
|
||||
vot-block[hidden]:not(.vot-menu):not(.vot-dialog-container),
|
||||
vot-block [hidden]:not(.vot-menu):not(.vot-dialog-container) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Use a stable typography baseline for all VOT UI.
|
||||
vot-block {
|
||||
--vot-font-family: "Roboto", "Segoe UI", system-ui, sans-serif !important;
|
||||
font-family: var(
|
||||
--vot-font-family,
|
||||
"Roboto",
|
||||
"Segoe UI",
|
||||
system-ui,
|
||||
sans-serif
|
||||
) !important;
|
||||
font-weight: 400 !important;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
display: block;
|
||||
// for reddit
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
// Keep host-page typography rules from overriding VOT font weights.
|
||||
vot-block * {
|
||||
font-weight: inherit !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,16 @@
|
|||
|
||||
@use "Button/IconButton.scss";
|
||||
@use "Button/TextButton.scss";
|
||||
@use "Button/OutlinedButton.scss";
|
||||
@use "Button/DetailsButton.scss";
|
||||
|
||||
@use "Control/Select.scss";
|
||||
@use "Control/Switch.scss";
|
||||
@use "Control/Slider.scss";
|
||||
@use "Control/SliderLabel.scss";
|
||||
@use "Control/SliderWrapper.scss";
|
||||
|
||||
@use "Settings/SettingsSection.scss";
|
||||
|
||||
@use "Textfield/Textfield.scss";
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,6 @@ export const proxyOnlyCountries: string[] = ["UA", "LV", "LT"];
|
|||
/**
|
||||
* 100 - 3000 ms - delay before hiding button
|
||||
*/
|
||||
export const defaultAutoHideDelay = 1000;
|
||||
export const DEFAULT_AUTO_HIDE_DELAY = 1000;
|
||||
|
||||
export const actualCompatVersion = "2025-05-09";
|
||||
|
|
|
|||
30
src/stores/settings.ts
Normal file
30
src/stores/settings.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { createStore } from "solid-js/store";
|
||||
|
||||
import type { Position } from "../types/components/votButton";
|
||||
|
||||
export type SettingsStore = {
|
||||
translateAPIErrors: boolean;
|
||||
newAudioPlayer: boolean;
|
||||
onlyBypassMediaCSP: boolean;
|
||||
showPiPButton: boolean;
|
||||
buttonPos: Position;
|
||||
};
|
||||
|
||||
function createInitialState(): SettingsStore {
|
||||
return {
|
||||
translateAPIErrors: true,
|
||||
// TODO: set default by audioContextSupported?
|
||||
newAudioPlayer: false,
|
||||
onlyBypassMediaCSP: false,
|
||||
showPiPButton: false,
|
||||
buttonPos: "default",
|
||||
};
|
||||
}
|
||||
|
||||
export const [settings, setSettings] = createStore<SettingsStore>(
|
||||
createInitialState(),
|
||||
);
|
||||
|
||||
export function resetSettings() {
|
||||
setSettings(createInitialState());
|
||||
}
|
||||
|
|
@ -1,15 +1,11 @@
|
|||
@use "../mixins" as shared;
|
||||
|
||||
.vot-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: rgba(var(--vot-helper-onsurface-rgb), 0.87);
|
||||
font-family: var(
|
||||
--vot-font-family,
|
||||
"Roboto",
|
||||
"Segoe UI",
|
||||
system-ui,
|
||||
sans-serif
|
||||
);
|
||||
color: rgba(var(--vot-onsurface-rgb), 0.87);
|
||||
@include shared.vot-font-family;
|
||||
font-size: 16px;
|
||||
border-radius: 0.5em !important;
|
||||
padding: 0.5em !important;
|
||||
|
|
@ -17,6 +13,7 @@
|
|||
line-height: 1.5;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
|
||||
&-arrow-icon {
|
||||
|
|
@ -26,10 +23,16 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: scale(1.25) rotate(-90deg);
|
||||
fill: rgba(var(--vot-helper-onsurface-rgb), 0.87);
|
||||
fill: rgba(var(--vot-onsurface-rgb), 0.87);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not([aria-disabled="true"]):hover {
|
||||
background: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.06);
|
||||
}
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.38;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,21 +17,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
.vot-settings-section-header {
|
||||
.vot-settings-section-header,
|
||||
.vot-settings-section__header {
|
||||
margin: 0 !important;
|
||||
padding: 0.45em 0.5em !important;
|
||||
border-radius: var(--vot-radius-m);
|
||||
}
|
||||
|
||||
.vot-settings-section-header .vot-details-arrow-icon {
|
||||
.vot-settings-section-header .vot-details-arrow-icon,
|
||||
.vot-settings-section__header .vot-details-arrow-icon {
|
||||
transition: transform var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
}
|
||||
|
||||
.vot-settings-section-header[data-open="true"] .vot-details-arrow-icon {
|
||||
.vot-settings-section-header[data-open="true"] .vot-details-arrow-icon,
|
||||
.vot-settings-section__header[data-open="true"] .vot-details-arrow-icon {
|
||||
transform: scale(1.25) rotate(0deg);
|
||||
}
|
||||
|
||||
.vot-settings-section-content {
|
||||
.vot-settings-section-content,
|
||||
.vot-settings-section__content {
|
||||
// Shared grid variables for "settings rows".
|
||||
// Components (select/slider) read these to align their right column.
|
||||
// Controls are no longer pushed to the far right edge of the dialog.
|
||||
|
|
@ -42,16 +46,13 @@
|
|||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--vot-settings-row-gap);
|
||||
padding: 0 var(--vot-space-1) var(--vot-space-1);
|
||||
|
||||
& > * {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
& > * + * {
|
||||
margin-top: var(--vot-settings-row-gap) !important;
|
||||
}
|
||||
|
||||
// Consistent padding + full-row hit area.
|
||||
// IMPORTANT: never override component `display` values here. Some controls
|
||||
// (hotkey, select, slider) rely on flex/grid internally.
|
||||
|
|
|
|||
|
|
@ -5,3 +5,7 @@ export type OnClickEvent = MouseEvent & {
|
|||
currentTarget: HTMLElement;
|
||||
target: Element;
|
||||
};
|
||||
|
||||
export type DataAttributes = {
|
||||
[K in `data-${string}`]?: string | number | boolean | undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,8 +42,10 @@ import { availableLangs } from "@vot.js/shared/consts";
|
|||
import { AboutSection } from "../../components/About/AboutSection";
|
||||
import { AccountMenu } from "../../components/Account/AccountMenu";
|
||||
import { Switch } from "../../components/Control/Switch";
|
||||
import { SettingsAppearanceSection } from "../../components/Settings/SettingsAppearanceSection";
|
||||
import { SettingsMiscSection } from "../../components/Settings/SettingsMiscSection";
|
||||
import { SettingsSection } from "../../components/Settings/SettingsSection";
|
||||
import {
|
||||
defaultAutoHideDelay,
|
||||
defaultAutoVolume,
|
||||
defaultDetectService,
|
||||
defaultTranslationService,
|
||||
|
|
@ -58,6 +60,8 @@ import {
|
|||
localizationProvider,
|
||||
} from "../../localization/localizationProvider";
|
||||
import { account, resetAccount, updateAccount } from "../../stores/account";
|
||||
import { setLocale } from "../../stores/locale";
|
||||
import { setSettings } from "../../stores/settings";
|
||||
import {
|
||||
getGoogleSubtitleFontFamilyName,
|
||||
loadGoogleFontsCatalog,
|
||||
|
|
@ -75,7 +79,7 @@ import type {
|
|||
LanguageSelectKey,
|
||||
SelectItem,
|
||||
} from "../../types/components/select";
|
||||
import { type Position, positions } from "../../types/components/votButton";
|
||||
import type { Position } from "../../types/components/votButton";
|
||||
import type {
|
||||
Account,
|
||||
ResponseLanguageSubtitles,
|
||||
|
|
@ -96,10 +100,8 @@ import debug from "../../utils/debug";
|
|||
import { EventImpl } from "../../utils/eventImpl";
|
||||
import { isProxyOnlyExtension, isSupportGMXhr } from "../../utils/gm";
|
||||
import { votStorage } from "../../utils/storage";
|
||||
import { isPiPAvailable } from "../../utils/utils";
|
||||
import type { VideoHandler } from "../../VideoHandler";
|
||||
import { getCountryCode } from "../../videoHandler/shared";
|
||||
import { normalizeButtonPosition } from "../buttonPlacement";
|
||||
import Checkbox from "../components/checkbox";
|
||||
import { createDomId } from "../components/componentShared";
|
||||
import Details from "../components/details";
|
||||
|
|
@ -217,7 +219,7 @@ export class SettingsView {
|
|||
void this.refreshAccountFromStorage();
|
||||
};
|
||||
dialog?: Dialog;
|
||||
accountMenu?: MountedComponent<HTMLElement>;
|
||||
accountSection?: MountedComponent<HTMLElement>;
|
||||
private accountStorageListenerCleanup?: () => void;
|
||||
autoTranslateSwitch?: MountedComponent<HTMLLabelElement>;
|
||||
autoSubtitlesSwitch?: MountedComponent<HTMLLabelElement>;
|
||||
|
|
@ -255,24 +257,13 @@ export class SettingsView {
|
|||
proxyTranslationStatusSelectLabel?: Label;
|
||||
proxyTranslationStatusSelectTooltip?: Tooltip;
|
||||
proxyTranslationStatusSelect?: Select;
|
||||
translateAPIErrorsCheckbox?: Checkbox;
|
||||
useNewAudioPlayerCheckbox?: Checkbox;
|
||||
useNewAudioPlayerTooltip?: Tooltip;
|
||||
onlyBypassMediaCSPCheckbox?: Checkbox;
|
||||
onlyBypassMediaCSPTooltip?: Tooltip;
|
||||
translationTextServiceLabel?: Label;
|
||||
translationTextServiceSelect?: Select<TranslateService>;
|
||||
translationTextServiceTooltip?: Tooltip;
|
||||
detectServiceLabel?: Label;
|
||||
detectServiceSelect?: Select<DetectService>;
|
||||
showPiPButtonCheckbox?: Checkbox;
|
||||
autoHideButtonDelaySliderLabel?: SliderLabel;
|
||||
autoHideButtonDelaySlider?: Slider;
|
||||
buttonPositionSelectLabel?: Label;
|
||||
buttonPositionSelect?: Select<Position>;
|
||||
buttonPositionTooltip?: Tooltip;
|
||||
menuLanguageSelectLabel?: Label;
|
||||
menuLanguageSelect?: Select<LangOverride>;
|
||||
appearanceSection?: MountedComponent<HTMLDivElement>;
|
||||
miscSection?: MountedComponent<HTMLDivElement>;
|
||||
aboutSection?: MountedComponent<HTMLDivElement>;
|
||||
bugReportButton?: HTMLElement;
|
||||
resetSettingsButton?: HTMLElement;
|
||||
|
|
@ -386,6 +377,16 @@ export class SettingsView {
|
|||
apply?.(value);
|
||||
void (async () => {
|
||||
await votStorage.set(storageKey, value);
|
||||
if (
|
||||
[
|
||||
"translateAPIErrors",
|
||||
"newAudioPlayer",
|
||||
"onlyBypassMediaCSP",
|
||||
].includes(storageKey)
|
||||
) {
|
||||
setSettings(storageKey as any, value);
|
||||
}
|
||||
|
||||
debug.log(`${logLabel} value changed. New value:`, value);
|
||||
await afterPersist?.(value);
|
||||
dispatch?.(value);
|
||||
|
|
@ -395,6 +396,26 @@ export class SettingsView {
|
|||
};
|
||||
}
|
||||
|
||||
private createBufferedNumericInputHandler({
|
||||
storageKey,
|
||||
logLabel = storageKey,
|
||||
dispatch,
|
||||
}: {
|
||||
storageKey: BufferedNumericStorageKey;
|
||||
logLabel?: string;
|
||||
dispatch?: (value: number) => void;
|
||||
}): (value: number) => void {
|
||||
return (value) => {
|
||||
this.data[storageKey] = value;
|
||||
if (storageKey === "autoHideButtonDelay") {
|
||||
setSettings(storageKey as any, value);
|
||||
}
|
||||
this.scheduleStoragePersist(storageKey, value);
|
||||
debug.log(`${logLabel} value changed. New value:`, value);
|
||||
dispatch?.(value);
|
||||
};
|
||||
}
|
||||
|
||||
private bindBufferedNumericSetting({
|
||||
control,
|
||||
label,
|
||||
|
|
@ -425,9 +446,6 @@ export class SettingsView {
|
|||
|
||||
private createSettingsSections() {
|
||||
const sections = [
|
||||
this.createAccordionSection(localizationProvider.get("VOTMyAccount"), {
|
||||
open: true,
|
||||
}),
|
||||
this.createAccordionSection(
|
||||
localizationProvider.get("translationSettings"),
|
||||
{ open: true },
|
||||
|
|
@ -437,42 +455,17 @@ export class SettingsView {
|
|||
),
|
||||
this.createAccordionSection(localizationProvider.get("hotkeysSettings")),
|
||||
this.createAccordionSection(localizationProvider.get("proxySettings")),
|
||||
this.createAccordionSection(localizationProvider.get("miscSettings")),
|
||||
this.createAccordionSection(localizationProvider.get("appearance")),
|
||||
this.createAccordionSection(localizationProvider.get("aboutExtension")),
|
||||
];
|
||||
|
||||
return {
|
||||
accountSection: sections[0],
|
||||
translationSection: sections[1],
|
||||
subtitlesSection: sections[2],
|
||||
hotkeysSection: sections[3],
|
||||
proxySection: sections[4],
|
||||
miscSection: sections[5],
|
||||
appearanceSection: sections[6],
|
||||
aboutSection: sections[7],
|
||||
translationSection: sections[0],
|
||||
subtitlesSection: sections[1],
|
||||
hotkeysSection: sections[2],
|
||||
proxySection: sections[3],
|
||||
sections,
|
||||
};
|
||||
}
|
||||
|
||||
private initAccountControls(): void {
|
||||
this.accountMenu = mountComponent((rootRef) =>
|
||||
AccountMenu({
|
||||
onClickLogin: async () => {
|
||||
debug.log("Account login button clicked");
|
||||
if (account.isLoggedIn) {
|
||||
await votStorage.delete("account");
|
||||
resetAccount();
|
||||
return this.updateAccountInfo();
|
||||
}
|
||||
|
||||
openAuthWindow();
|
||||
},
|
||||
ref: rootRef,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private bindAccountStorageListener(): void {
|
||||
this.accountStorageListenerCleanup?.();
|
||||
this.accountStorageListenerCleanup = votStorage.addValueChangeListener<
|
||||
|
|
@ -578,21 +571,101 @@ export class SettingsView {
|
|||
titleHtml: localizationProvider.get("VOTSettings"),
|
||||
});
|
||||
this.globalPortal.appendChild(this.dialog.container);
|
||||
this.accountSection = mountComponent<HTMLDivElement>((rootRef) =>
|
||||
SettingsSection({
|
||||
title: localizationProvider.get("VOTMyAccount"),
|
||||
isOpen: true,
|
||||
ref: rootRef,
|
||||
children: AccountMenu({
|
||||
onClickLogin: async () => {
|
||||
debug.log("Account login button clicked");
|
||||
if (account.isLoggedIn) {
|
||||
await votStorage.delete("account");
|
||||
resetAccount();
|
||||
return this.updateAccountInfo();
|
||||
}
|
||||
|
||||
openAuthWindow();
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
this.appearanceSection = mountComponent<HTMLDivElement>((rootRef) =>
|
||||
SettingsAppearanceSection({
|
||||
ref: rootRef,
|
||||
onShowPiPButtonChange: this.createPersistedSettingHandler({
|
||||
storageKey: "showPiPButton",
|
||||
dispatch: (checked) =>
|
||||
this.events["change:showPiPButton"].dispatch(checked),
|
||||
}),
|
||||
onAutoHideButtonDelayInput: this.createBufferedNumericInputHandler({
|
||||
storageKey: "autoHideButtonDelay",
|
||||
dispatch: (value) =>
|
||||
this.events["input:autoHideButtonDelay"].dispatch(value),
|
||||
}),
|
||||
onButtonPositionSelect: (option) =>
|
||||
this.createPersistedSettingHandler({
|
||||
storageKey: "buttonPos",
|
||||
dispatch: (item) =>
|
||||
this.events["select:buttonPosition"].dispatch(item),
|
||||
})(option.value as Position),
|
||||
onLangSelect: async (option) => {
|
||||
const item = option.value as LangOverride;
|
||||
const result = await localizationProvider.changeLang(item);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.data.localeUpdatedAt = await votStorage.get(
|
||||
"localeUpdatedAt",
|
||||
0,
|
||||
);
|
||||
setLocale("updatedAt", 0);
|
||||
this.events["select:menuLanguage"].dispatch(item);
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.miscSection = mountComponent<HTMLDivElement>((rootRef) =>
|
||||
SettingsMiscSection({
|
||||
ref: rootRef,
|
||||
onChangeTranslateAPIErrors: this.createPersistedSettingHandler({
|
||||
storageKey: "translateAPIErrors",
|
||||
}),
|
||||
isAudioContextSupported: this.videoHandler?.isAudioContextSupported,
|
||||
needBypassCSP: this.videoHandler.site.needBypassCSP,
|
||||
onChangeNewAudioPlayer: this.createPersistedSettingHandler({
|
||||
storageKey: "newAudioPlayer",
|
||||
dispatch: (checked) =>
|
||||
this.events["change:useNewAudioPlayer"].dispatch(checked),
|
||||
}),
|
||||
onChangeOnlyBypassMediaCSP: this.createPersistedSettingHandler({
|
||||
storageKey: "onlyBypassMediaCSP",
|
||||
dispatch: (checked) =>
|
||||
this.events["change:onlyBypassMediaCSP"].dispatch(checked),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
this.aboutSection = mountComponent<HTMLDivElement>((rootRef) =>
|
||||
SettingsSection({
|
||||
title: localizationProvider.get("aboutExtension"),
|
||||
ref: rootRef,
|
||||
children: AboutSection({}),
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
accountSection,
|
||||
translationSection,
|
||||
subtitlesSection,
|
||||
hotkeysSection,
|
||||
proxySection,
|
||||
miscSection,
|
||||
appearanceSection,
|
||||
aboutSection,
|
||||
sections,
|
||||
} = this.createSettingsSections();
|
||||
this.dialog.bodyContainer.append(
|
||||
this.accountSection.root,
|
||||
...sections.map((section) => section.container),
|
||||
this.miscSection.root,
|
||||
this.appearanceSection.root,
|
||||
this.aboutSection.root,
|
||||
);
|
||||
this.initAccountControls();
|
||||
this.autoTranslateSwitch = mountComponent<HTMLLabelElement>((rootRef) =>
|
||||
Switch({
|
||||
heading: localizationProvider.get("VOTAutoTranslate"),
|
||||
|
|
@ -727,7 +800,6 @@ export class SettingsView {
|
|||
backgroundColor: "var(--vot-helper-ondialog)",
|
||||
parentElement: this.globalPortal,
|
||||
});
|
||||
accountSection.content.append(this.accountMenu.root);
|
||||
|
||||
translationSection.content.append(
|
||||
this.autoTranslateSwitch.root,
|
||||
|
|
@ -916,47 +988,6 @@ export class SettingsView {
|
|||
this.proxyWorkerHostTextfield.container,
|
||||
this.proxyTranslationStatusSelect.container,
|
||||
);
|
||||
this.translateAPIErrorsCheckbox = new Checkbox({
|
||||
labelHtml: localizationProvider.get("VOTTranslateAPIErrors"),
|
||||
checked: this.data.translateAPIErrors ?? true,
|
||||
});
|
||||
this.translateAPIErrorsCheckbox.hidden = localizationProvider.lang === "ru";
|
||||
this.useNewAudioPlayerCheckbox = new Checkbox({
|
||||
labelHtml: localizationProvider.get("VOTNewAudioPlayer"),
|
||||
checked: this.data.newAudioPlayer,
|
||||
});
|
||||
if (!this.videoHandler?.isAudioContextSupported) {
|
||||
this.useNewAudioPlayerCheckbox.disabled = true;
|
||||
this.useNewAudioPlayerTooltip = new Tooltip({
|
||||
target: this.useNewAudioPlayerCheckbox.container,
|
||||
content: localizationProvider.get("VOTNeedWebAudioAPI"),
|
||||
position: "bottom",
|
||||
backgroundColor: "var(--vot-helper-ondialog)",
|
||||
parentElement: this.globalPortal,
|
||||
});
|
||||
}
|
||||
const onlyBypassMediaCSPLabel = this.videoHandler?.site.needBypassCSP
|
||||
? `${localizationProvider.get("VOTOnlyBypassMediaCSP")} (${localizationProvider.get("VOTMediaCSPEnabledOnSite")})`
|
||||
: localizationProvider.get("VOTOnlyBypassMediaCSP");
|
||||
this.onlyBypassMediaCSPCheckbox = new Checkbox({
|
||||
labelHtml: onlyBypassMediaCSPLabel,
|
||||
checked: this.data.onlyBypassMediaCSP,
|
||||
isSubCheckbox: true,
|
||||
});
|
||||
if (!this.videoHandler?.isAudioContextSupported) {
|
||||
this.onlyBypassMediaCSPTooltip = new Tooltip({
|
||||
target: this.onlyBypassMediaCSPCheckbox.container,
|
||||
content: localizationProvider.get("VOTNeedWebAudioAPI"),
|
||||
position: "bottom",
|
||||
backgroundColor: "var(--vot-helper-ondialog)",
|
||||
parentElement: this.globalPortal,
|
||||
});
|
||||
}
|
||||
this.onlyBypassMediaCSPCheckbox.disabled =
|
||||
!this.data.newAudioPlayer && !!this.videoHandler?.isAudioContextSupported;
|
||||
if (!this.data.newAudioPlayer) {
|
||||
this.onlyBypassMediaCSPCheckbox.hidden = true;
|
||||
}
|
||||
this.translationTextServiceLabel = new Label({
|
||||
labelText: localizationProvider.get("VOTTranslationTextService"),
|
||||
icon: HELP_ICON,
|
||||
|
|
@ -996,94 +1027,17 @@ export class SettingsView {
|
|||
selected: service === detectService,
|
||||
})),
|
||||
});
|
||||
this.showPiPButtonCheckbox = new Checkbox({
|
||||
labelHtml: localizationProvider.get("VOTShowPiPButton"),
|
||||
checked: this.data.showPiPButton,
|
||||
});
|
||||
this.showPiPButtonCheckbox.hidden = !isPiPAvailable();
|
||||
const autoHideButtonDelaySec =
|
||||
Math.round(
|
||||
((this.data.autoHideButtonDelay ?? defaultAutoHideDelay) / 1000) * 10,
|
||||
) / 10;
|
||||
this.autoHideButtonDelaySliderLabel = new SliderLabel({
|
||||
labelText: localizationProvider.get("autoHideButtonDelay"),
|
||||
labelEOL: ":",
|
||||
value: autoHideButtonDelaySec,
|
||||
symbol: ` ${localizationProvider.get("secs")}`,
|
||||
});
|
||||
this.autoHideButtonDelaySlider = new Slider({
|
||||
labelHtml: this.autoHideButtonDelaySliderLabel.container,
|
||||
value: autoHideButtonDelaySec,
|
||||
min: 0.1,
|
||||
max: 3,
|
||||
step: 0.1,
|
||||
});
|
||||
this.buttonPositionSelectLabel = new Label({
|
||||
labelText: localizationProvider.get("buttonPosition"),
|
||||
icon: HELP_ICON,
|
||||
});
|
||||
const buttonPos = normalizeButtonPosition(this.data.buttonPos);
|
||||
this.buttonPositionSelect = new Select<Position>({
|
||||
selectTitle: localizationProvider.get(`position.${buttonPos}`),
|
||||
dialogTitle: localizationProvider.get("buttonPosition"),
|
||||
labelElement: this.buttonPositionSelectLabel.container,
|
||||
dialogParent: this.globalPortal,
|
||||
items: positions.map<SelectItem<Position>>((position) => ({
|
||||
label: localizationProvider.get(`position.${position}`),
|
||||
value: position,
|
||||
selected: position === buttonPos,
|
||||
})),
|
||||
});
|
||||
this.buttonPositionTooltip = new Tooltip({
|
||||
target: this.buttonPositionSelectLabel.icon,
|
||||
content: localizationProvider.get("minButtonPositionContainer"),
|
||||
position: "bottom",
|
||||
backgroundColor: "var(--vot-helper-ondialog)",
|
||||
parentElement: this.globalPortal,
|
||||
});
|
||||
this.menuLanguageSelectLabel = new Label({
|
||||
labelText: localizationProvider.get("VOTMenuLanguage"),
|
||||
});
|
||||
this.menuLanguageSelect = new Select<LangOverride>({
|
||||
selectTitle: localizationProvider.get(
|
||||
`langs.${localizationProvider.langOverride}` as any,
|
||||
),
|
||||
dialogTitle: localizationProvider.get("VOTMenuLanguage"),
|
||||
labelElement: this.menuLanguageSelectLabel.container,
|
||||
dialogParent: this.globalPortal,
|
||||
items: Select.genLanguageItems(
|
||||
localizationProvider.getAvailableLangs(),
|
||||
localizationProvider.langOverride,
|
||||
),
|
||||
});
|
||||
this.bugReportButton = ui.createOutlinedButton(
|
||||
localizationProvider.get("VOTBugReport"),
|
||||
);
|
||||
this.resetSettingsButton = ui.createButton(
|
||||
localizationProvider.get("resetSettings"),
|
||||
);
|
||||
miscSection.content.append(
|
||||
this.translateAPIErrorsCheckbox.container,
|
||||
this.useNewAudioPlayerCheckbox.container,
|
||||
this.onlyBypassMediaCSPCheckbox.container,
|
||||
);
|
||||
translationSection.content.append(
|
||||
this.translationTextServiceSelect.container,
|
||||
this.detectServiceSelect.container,
|
||||
);
|
||||
appearanceSection.content.append(
|
||||
this.showPiPButtonCheckbox.container,
|
||||
this.autoHideButtonDelaySlider.container,
|
||||
this.buttonPositionSelect.container,
|
||||
this.menuLanguageSelect.container,
|
||||
);
|
||||
this.aboutSection = mountComponent<HTMLDivElement>((rootRef) =>
|
||||
AboutSection({
|
||||
ref: rootRef,
|
||||
}),
|
||||
);
|
||||
|
||||
aboutSection.content.append(this.aboutSection.root);
|
||||
this.dialog.footerContainer.append(
|
||||
this.bugReportButton,
|
||||
this.resetSettingsButton,
|
||||
|
|
@ -1300,32 +1254,7 @@ export class SettingsView {
|
|||
this.events["select:proxyTranslationStatus"].dispatch(item);
|
||||
},
|
||||
);
|
||||
this.translateAPIErrorsCheckbox.addEventListener(
|
||||
"change",
|
||||
this.createPersistedSettingHandler({
|
||||
storageKey: "translateAPIErrors",
|
||||
}),
|
||||
);
|
||||
this.useNewAudioPlayerCheckbox.addEventListener(
|
||||
"change",
|
||||
this.createPersistedSettingHandler({
|
||||
storageKey: "newAudioPlayer",
|
||||
apply: (checked) => {
|
||||
this.onlyBypassMediaCSPCheckbox.disabled =
|
||||
this.onlyBypassMediaCSPCheckbox.hidden = !checked;
|
||||
},
|
||||
dispatch: (checked) =>
|
||||
this.events["change:useNewAudioPlayer"].dispatch(checked),
|
||||
}),
|
||||
);
|
||||
this.onlyBypassMediaCSPCheckbox.addEventListener(
|
||||
"change",
|
||||
this.createPersistedSettingHandler({
|
||||
storageKey: "onlyBypassMediaCSP",
|
||||
dispatch: (checked) =>
|
||||
this.events["change:onlyBypassMediaCSP"].dispatch(checked),
|
||||
}),
|
||||
);
|
||||
|
||||
this.translationTextServiceSelect.addEventListener(
|
||||
"selectItem",
|
||||
this.createPersistedSettingHandler({
|
||||
|
|
@ -1340,36 +1269,6 @@ export class SettingsView {
|
|||
storageKey: "detectService",
|
||||
}),
|
||||
);
|
||||
this.showPiPButtonCheckbox.addEventListener(
|
||||
"change",
|
||||
this.createPersistedSettingHandler({
|
||||
storageKey: "showPiPButton",
|
||||
dispatch: (checked) =>
|
||||
this.events["change:showPiPButton"].dispatch(checked),
|
||||
}),
|
||||
);
|
||||
this.bindBufferedNumericSetting({
|
||||
control: this.autoHideButtonDelaySlider,
|
||||
label: this.autoHideButtonDelaySliderLabel,
|
||||
storageKey: "autoHideButtonDelay",
|
||||
logLabel: "autoHideButtonDelay",
|
||||
toStoredValue: (value) => Math.round(value * 1000),
|
||||
dispatch: (value) =>
|
||||
this.events["input:autoHideButtonDelay"].dispatch(value),
|
||||
});
|
||||
this.buttonPositionSelect.addEventListener(
|
||||
"selectItem",
|
||||
this.createPersistedSettingHandler({
|
||||
storageKey: "buttonPos",
|
||||
dispatch: (item) => this.events["select:buttonPosition"].dispatch(item),
|
||||
}),
|
||||
);
|
||||
this.menuLanguageSelect.addEventListener("selectItem", async (item) => {
|
||||
const result = await localizationProvider.changeLang(item);
|
||||
if (!result) return;
|
||||
this.data.localeUpdatedAt = await votStorage.get("localeUpdatedAt", 0);
|
||||
this.events["select:menuLanguage"].dispatch(item);
|
||||
});
|
||||
this.bugReportButton.addEventListener("click", () =>
|
||||
this.events["click:bugReport"].dispatch(),
|
||||
);
|
||||
|
|
@ -1394,9 +1293,11 @@ export class SettingsView {
|
|||
}
|
||||
private doReleaseUI(): void {
|
||||
for (const key of [
|
||||
"accountSection",
|
||||
"autoTranslateSwitch",
|
||||
"autoSubtitlesSwitch",
|
||||
"accountMenu",
|
||||
"appearanceSection",
|
||||
"miscSection",
|
||||
"aboutSection",
|
||||
] satisfies (keyof (typeof SettingsView)["prototype"])[]) {
|
||||
const control = this[key] as MountedComponent<any> | undefined;
|
||||
|
|
@ -1408,11 +1309,8 @@ export class SettingsView {
|
|||
this.dialog?.remove();
|
||||
for (const tooltip of [
|
||||
this.useAudioDownloadCheckboxTooltip,
|
||||
this.useNewAudioPlayerTooltip,
|
||||
this.onlyBypassMediaCSPTooltip,
|
||||
this.translationTextServiceTooltip,
|
||||
this.proxyTranslationStatusSelectTooltip,
|
||||
this.buttonPositionTooltip,
|
||||
]) {
|
||||
tooltip?.release();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import YoutubeHelper from "@vot.js/ext/helpers/youtube";
|
|||
import { getVideoID } from "@vot.js/ext/utils/videoData";
|
||||
import { availableLangs } from "@vot.js/shared/consts";
|
||||
import type { RequestLang } from "@vot.js/shared/types/data";
|
||||
import { defaultAutoHideDelay } from "../../config/config";
|
||||
import { DEFAULT_AUTO_HIDE_DELAY } from "../../config/config";
|
||||
import {
|
||||
isDesktopYouTubeLikeSite,
|
||||
isMuteSyncDisabledHost,
|
||||
|
|
@ -610,7 +610,7 @@ export function getAutoHideDelay(this: VideoHandler): number {
|
|||
const delay = this.data?.autoHideButtonDelay;
|
||||
return typeof delay === "number" && Number.isFinite(delay)
|
||||
? delay
|
||||
: defaultAutoHideDelay;
|
||||
: DEFAULT_AUTO_HIDE_DELAY;
|
||||
}
|
||||
export function releaseExtraEvents(this: VideoHandler) {
|
||||
this.resizeObserver?.disconnect();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
actualCompatVersion,
|
||||
defaultAutoHideDelay,
|
||||
DEFAULT_AUTO_HIDE_DELAY,
|
||||
defaultAutoVolume,
|
||||
defaultDetectService,
|
||||
defaultTranslationService,
|
||||
|
|
@ -10,7 +10,9 @@ import {
|
|||
} from "../../config/config";
|
||||
import { updateAccountFromStorage } from "../../stores/account";
|
||||
import { setLocale } from "../../stores/locale";
|
||||
import { setSettings } from "../../stores/settings";
|
||||
import type { LanguageSelectKey } from "../../types/components/select";
|
||||
import { normalizeButtonPosition } from "../../ui/buttonPlacement";
|
||||
import debug from "../../utils/debug";
|
||||
import { GM_fetch, isProxyOnlyExtension, isSupportGMXhr } from "../../utils/gm";
|
||||
import { updateConfig, votStorage } from "../../utils/storage";
|
||||
|
|
@ -89,7 +91,7 @@ export async function init(this: VideoHandler) {
|
|||
translateProxyEnabledDefault: true,
|
||||
audioBooster: false,
|
||||
useLivelyVoice: false,
|
||||
autoHideButtonDelay: defaultAutoHideDelay,
|
||||
autoHideButtonDelay: DEFAULT_AUTO_HIDE_DELAY,
|
||||
// Audio download now uses direct network requests (GM_fetch/GM_xmlhttpRequest).
|
||||
useAudioDownload: isSupportGMXhr,
|
||||
compatVersion: "",
|
||||
|
|
@ -108,6 +110,13 @@ export async function init(this: VideoHandler) {
|
|||
updatedAt: this.data.localeUpdatedAt,
|
||||
hash: this.data.localeHash,
|
||||
});
|
||||
setSettings({
|
||||
translateAPIErrors: this.data.translateAPIErrors,
|
||||
newAudioPlayer: this.data.newAudioPlayer,
|
||||
onlyBypassMediaCSP: this.data.onlyBypassMediaCSP,
|
||||
showPiPButton: this.data.showPiPButton,
|
||||
buttonPos: normalizeButtonPosition(this.data.buttonPos),
|
||||
});
|
||||
|
||||
try {
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { srcDir } from "../paths.ts";
|
|||
const localesDir = path.resolve(srcDir, "localization", "locales");
|
||||
const localeHeadersDir = path.resolve(localesDir, "headers");
|
||||
const hashesPath = path.resolve(srcDir, "localization", "hashes.json");
|
||||
const _metaHeadersPath = path.resolve(srcDir, "headers.json");
|
||||
|
||||
const priorityLocales = ["auto", "en", "ru"] as const;
|
||||
type PriorityLocale = (typeof priorityLocales)[number];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue