refactor: rewrited AboutSection with Solid

This commit is contained in:
Toil 2026-08-15 02:52:27 +03:00
parent bfbfce1cc3
commit 166a66ccd0
No known key found for this signature in database
GPG key ID: 57248E2819D32070
27 changed files with 943 additions and 793 deletions

View file

@ -45,6 +45,7 @@ const config: StorybookConfig = {
],
define: {
DEBUG_MODE: true,
GM_info: {},
},
resolve: {
alias: {

View file

@ -1,5 +1,8 @@
# 1.12.0 (WIP)
- Переработан дизайн блока настроек "Мой аккаунт"
- Переработан дизайн блока "О расширении"
- Все `Checkbox` в настройках были заменены на `Switch`
- Весь UI расширения переведен на SolidJS
- Исправлена ошибка из-за которой Debug логи попадали в релизную сборку расширения

Binary file not shown.

Binary file not shown.

22
dist/vot-min.user.js vendored

File diff suppressed because one or more lines are too long

1331
dist/vot.user.js vendored

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,17 @@
.vot-about-item {
display: flex;
gap: 0.5em;
align-items: center;
justify-content: space-between;
font-size: 0.8em;
&__label {
text-transform: lowercase;
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.68);
}
&__value {
text-align: end;
color: rgba(var(--vot-primary-rgb, 33, 150, 243), 0.9);
}
}

View file

@ -0,0 +1,18 @@
import type { JSX } from "solid-js";
import "./AboutItem.scss";
export type AccountInfoProps = {
label: string;
children: JSX.Element;
ref?: (element: HTMLElement) => void;
};
export function AboutItem(props: AccountInfoProps): JSX.Element {
return (
<vot-block ref={props.ref} class="vot-about-item">
<vot-block class="vot-about-item__label">{props.label}</vot-block>
<vot-block class="vot-about-item__value">{props.children}</vot-block>
</vot-block>
);
}

View file

@ -0,0 +1,12 @@
.vot-about-section {
--vot-about-sep-color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.12);
display: flex;
flex-direction: column;
gap: 0.5em;
> .vot-about-item:not(:last-child) {
border-bottom: 1px solid var(--vot-about-sep-color);
padding-bottom: 1em;
}
}

View file

@ -0,0 +1,14 @@
import type { Meta, StoryObj } from "storybook-solidjs-vite";
import { AboutSection } from "./AboutSection";
const meta = {
component: AboutSection,
} satisfies Meta<typeof AboutSection>;
export default meta;
type Story = StoryObj<typeof meta>;
export const AboutSectionDefault: Story = {
args: {},
};

View file

@ -0,0 +1,67 @@
import type { TMInfoScriptMeta } from "@toil/gm-types/types/info/tampermonkey";
import type { JSX } from "solid-js";
import { AboutItem } from "./AboutItem";
import "./AboutSection.scss";
import { localizationProvider } from "../../localization/localizationProvider";
import { locale } from "../../stores/locale";
import { getEnvironmentInfo } from "../../utils/environment";
import { votStorage } from "../../utils/storage";
import { OutlinedButton } from "../Button/OutlinedButton";
export type AccountSectionProps = {
ref?: (element: HTMLElement) => void;
};
export function AboutSection(props: AccountSectionProps): JSX.Element {
const envInfo = getEnvironmentInfo();
const safeGMInfo = typeof GM_info === "undefined" ? undefined : GM_info;
const scriptVersion = () =>
envInfo.scriptVersion === "unknown"
? safeGMInfo?.script?.version || localizationProvider.get("notFound")
: envInfo.scriptVersion;
const buildAuthors = () =>
typeof VOT_AUTHORS === "undefined" ? "" : String(VOT_AUTHORS);
const scriptAuthors = () =>
(safeGMInfo?.script as TMInfoScriptMeta)?.author ||
buildAuthors() ||
localizationProvider.get("notFound");
const loaderInfo = () => envInfo.loader;
const browserInfo = () => `${envInfo.browser} (${envInfo.os})`;
const localeUpdatedAt = () =>
new Date(locale.updatedAt * 1000).toLocaleString();
const localeHashValue = () =>
locale.hash || localizationProvider.get("notFound");
return (
<vot-block ref={props.ref} class="vot-about-section">
<AboutItem label={localizationProvider.get("VOTVersion")}>
{scriptVersion()}
</AboutItem>
<AboutItem label={localizationProvider.get("VOTAuthors")}>
{scriptAuthors()}
</AboutItem>
<AboutItem label={localizationProvider.get("VOTLoader")}>
{loaderInfo()}
</AboutItem>
<AboutItem label={localizationProvider.get("VOTBrowser")}>
{browserInfo()}
</AboutItem>
<AboutItem label={localizationProvider.get("VOTLocaleHash")}>
{localeHashValue()}
<br />({localizationProvider.get("VOTUpdatedAt")} {localeUpdatedAt})
</AboutItem>
<OutlinedButton
onClick={async () => {
await votStorage.set("localeHash", "");
await localizationProvider.update(true);
globalThis.location.reload();
}}
>
{localizationProvider.get("VOTUpdateLocaleFiles")}
</OutlinedButton>
</vot-block>
);
}

View file

@ -9,7 +9,7 @@
&__block {
display: flex;
gap: 0.5rem;
gap: 0.5em;
align-items: center;
}

View file

@ -1,4 +1,5 @@
import type { JSX } from "solid-js";
import "./IconButton.scss";
import { RawButton, type RawButtonProps } from "./RawButton";

View file

@ -0,0 +1 @@
@use "../../styles/components/buttons/outlined.scss";

View file

@ -0,0 +1,42 @@
import type { Meta, StoryObj } from "storybook-solidjs-vite";
import { OutlinedButton } from "./OutlinedButton";
const meta = {
component: OutlinedButton,
} satisfies Meta<typeof OutlinedButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const OutlinedButtonDefault: Story = {
args: {
children: "Settings",
},
};
export const OutlinedButtonWithOnClick: Story = {
args: {
children: "Settings",
onClick: () => {
alert("hi");
},
},
};
export const OutlinedButtonDisabled: Story = {
args: {
children: "Settings",
disabled: true,
onClick: () => {
alert("you shouldn't see this");
},
},
};
export const OutlinedButtonHidden: Story = {
args: {
children: "Settings",
hidden: true,
},
};

View file

@ -0,0 +1,10 @@
import type { JSX } from "solid-js";
import "./OutlinedButton.scss";
import { RawButton, type RawButtonProps } from "./RawButton";
export type OutlinedButtonProps = Omit<RawButtonProps, "class">;
export function OutlinedButton(props: OutlinedButtonProps): JSX.Element {
return <RawButton {...props} class="vot-outlined-button" />;
}

View file

@ -1,4 +1,5 @@
import type { JSX } from "solid-js";
import "./TextButton.scss";
import { RawButton, type RawButtonProps } from "./RawButton";

View file

@ -1,10 +1,12 @@
import type { JSX } from "solid-js";
import { AboutSection } from "../About/AboutSection";
import { AccountMenu } from "../Account/AccountMenu";
export function SettingsContent(): JSX.Element {
return (
<vot-block style="max-width: 450px;display:block;margin:0 auto;">
<AccountMenu />
<AboutSection />
</vot-block>
);
}

View file

@ -1,5 +1,8 @@
@use "global.scss";
@use "About/AboutItem.scss";
@use "About/AboutSection.scss";
@use "Account/AccountInfo.scss";
@use "Account/AccountLogin.scss";
@use "Account/AccountLogout.scss";

1
src/global.d.ts vendored
View file

@ -35,7 +35,6 @@ declare global {
// These are injected at runtime by the userscript manager or by our extension
// prelude. GM is installed synchronously at document_start before any
// content-script code evaluates.
const GM_info: any;
const GM: {
// Promise-based GM API (supported by most userscript managers).

21
src/stores/locale.ts Normal file
View file

@ -0,0 +1,21 @@
import { createStore } from "solid-js/store";
export type LocaleStore = {
updatedAt: number;
hash: string;
};
function createInitialState(): LocaleStore {
return {
updatedAt: 0,
hash: "",
};
}
export const [locale, setLocale] = createStore<LocaleStore>(
createInitialState(),
);
export function resetLocale() {
setLocale(createInitialState());
}

View file

@ -1,28 +0,0 @@
.vot-account {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
&-container,
&-wrapper,
&-buttons {
display: flex;
gap: 1rem;
align-items: center;
}
&-avatar {
max-width: 36px;
max-height: 36px;
min-width: 36px;
min-height: 36px;
overflow: hidden;
&-img {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
}
}
}

View file

@ -20,7 +20,8 @@
@include button.vot-button-layer-hover(0.04);
@include button.vot-button-layer-active(0.16);
&[disabled="true"] {
&[disabled="true"],
&[aria-disabled="true"] {
background-color: transparent;
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
cursor: initial;

View file

@ -21,7 +21,6 @@
@use "components/dialog.scss";
@use "components/loader.scss";
@use "components/label.scss";
@use "components/account.scss";
@use "subtitles.scss";
:root {
@ -58,8 +57,7 @@
--vot-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.18), 0 8px 24px rgba(0, 0, 0, 0.14);
--vot-shadow-2:
0 2px 4px rgba(0, 0, 0, 0.22),
0 12px 32px rgba(0, 0, 0, 0.22);
0 2px 4px rgba(0, 0, 0, 0.22), 0 12px 32px rgba(0, 0, 0, 0.22);
--vot-duration-fast: 120ms;
--vot-duration-medium: 200ms;

View file

@ -40,14 +40,6 @@ function initKeyboardNavigationMode(): void {
// Initialize once at module load.
initKeyboardNavigationMode();
type HeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
type InformationElements = {
container: HTMLElement;
header: HTMLElement;
value: HTMLElement;
};
type SubtitleInfoElements = {
container: HTMLElement;
translatedWith: HTMLElement;
@ -151,33 +143,6 @@ const UI = {
return el;
},
/**
* Create header element
*/
createHeader(html: Node | string, level: HeaderLevel = 4): HTMLElement {
return UI.createEl(
"vot-block",
["vot-header", `vot-header-level-${level}`],
html,
);
},
/**
* Create information element
*/
createInformation(
labelHtml: UiTemplate,
valueHtml: UiTemplate,
): InformationElements {
const container = UI.createEl("vot-block", ["vot-info"]);
const header = UI.createEl("vot-block");
appendTemplate(labelHtml, header);
const value = UI.createEl("vot-block");
appendTemplate(valueHtml, value);
container.append(header, value);
return { container, header, value };
},
/**
* Create button
*/
@ -210,10 +175,6 @@ const UI = {
return UI.createEl("vot-block", ["vot-inline-loader"]);
},
createPortal(local: boolean = false): HTMLElement {
return UI.createEl("vot-block", [`vot-portal${local ? "-local" : ""}`]);
},
createSubtitleInfo(
word: string,
desc: string,

View file

@ -1,7 +1,3 @@
type TMInfoScriptMeta = {
author?: string;
[key: string]: unknown;
};
const SETTINGS_EVENT_KEYS: Array<keyof SettingsViewEventMap> = [
"click:bugReport",
"click:resetSettings",
@ -43,6 +39,7 @@ function createSettingsEvents(): {
}
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 {
@ -60,12 +57,7 @@ import {
type LangOverride,
localizationProvider,
} from "../../localization/localizationProvider";
import {
account,
resetAccount,
updateAccount,
updateAccountFromStorage,
} from "../../stores/account";
import { account, resetAccount, updateAccount } from "../../stores/account";
import {
getGoogleSubtitleFontFamilyName,
loadGoogleFontsCatalog,
@ -101,7 +93,6 @@ import type {
} from "../../types/views/settings";
import ui from "../../ui";
import debug from "../../utils/debug";
import { getEnvironmentInfo } from "../../utils/environment";
import { EventImpl } from "../../utils/eventImpl";
import { isProxyOnlyExtension, isSupportGMXhr } from "../../utils/gm";
import { votStorage } from "../../utils/storage";
@ -282,6 +273,7 @@ export class SettingsView {
buttonPositionTooltip?: Tooltip;
menuLanguageSelectLabel?: Label;
menuLanguageSelect?: Select<LangOverride>;
aboutSection?: MountedComponent<HTMLDivElement>;
bugReportButton?: HTMLElement;
resetSettingsButton?: HTMLElement;
constructor({ globalPortal, data = {}, videoHandler }: SettingsViewProps) {
@ -578,71 +570,6 @@ export class SettingsView {
return this.buildSubtitleFontItems(activeFontFamily, matchingGoogleFonts);
}
private buildAboutSectionContent(aboutSection: {
content: HTMLElement;
}): void {
const envInfo = getEnvironmentInfo();
const safeGMInfo = typeof GM_info === "undefined" ? undefined : GM_info;
const versionInfo = ui.createInformation(
`${localizationProvider.get("VOTVersion")}:`,
envInfo.scriptVersion === "unknown"
? safeGMInfo?.script?.version || localizationProvider.get("notFound")
: envInfo.scriptVersion,
);
const buildAuthors =
typeof VOT_AUTHORS === "undefined" ? "" : String(VOT_AUTHORS);
const authorsInfo = ui.createInformation(
`${localizationProvider.get("VOTAuthors")}:`,
(safeGMInfo?.script as TMInfoScriptMeta | undefined)?.author ||
buildAuthors ||
localizationProvider.get("notFound"),
);
const loaderInfo = ui.createInformation(
`${localizationProvider.get("VOTLoader")}:`,
envInfo.loader,
);
const userBrowserInfo = ui.createInformation(
`${localizationProvider.get("VOTBrowser")}:`,
`${envInfo.browser} (${envInfo.os})`,
);
const localeUpdatedAt = new Date(
(this.data.localeUpdatedAt ?? 0) * 1000,
).toLocaleString();
const localeHashValue =
this.data.localeHash ?? localizationProvider.get("notFound");
// Solid: a thunk building the same two-line fragment. No reactive value is
// involved (the strings are computed once per settings render), so a plain
const localeInfoValue = () => {
const fragment = document.createDocumentFragment();
fragment.append(
localeHashValue,
document.createElement("br"),
`(${localizationProvider.get("VOTUpdatedAt")} ${localeUpdatedAt})`,
);
return fragment;
};
const localeInfo = ui.createInformation(
`${localizationProvider.get("VOTLocaleHash")}:`,
localeInfoValue,
);
const updateLocaleFilesButton = ui.createOutlinedButton(
localizationProvider.get("VOTUpdateLocaleFiles"),
);
updateLocaleFilesButton.addEventListener("click", async () => {
await votStorage.set("localeHash", "");
await localizationProvider.update(true);
globalThis.location.reload();
});
aboutSection.content.append(
versionInfo.container,
authorsInfo.container,
loaderInfo.container,
userBrowserInfo.container,
localeInfo.container,
updateLocaleFilesButton,
);
}
initUI() {
if (this.isInitialized()) {
throw new Error("[VOT] SettingsView is already initialized");
@ -1150,7 +1077,13 @@ export class SettingsView {
this.buttonPositionSelect.container,
this.menuLanguageSelect.container,
);
this.buildAboutSectionContent(aboutSection);
this.aboutSection = mountComponent<HTMLDivElement>((rootRef) =>
AboutSection({
ref: rootRef,
}),
);
aboutSection.content.append(this.aboutSection.root);
this.dialog.footerContainer.append(
this.bugReportButton,
this.resetSettingsButton,
@ -1464,6 +1397,7 @@ export class SettingsView {
"autoTranslateSwitch",
"autoSubtitlesSwitch",
"accountMenu",
"aboutSection",
] satisfies (keyof (typeof SettingsView)["prototype"])[]) {
const control = this[key] as MountedComponent<any> | undefined;
control?.dispose();

View file

@ -9,6 +9,7 @@ import {
proxyWorkerHost,
} from "../../config/config";
import { updateAccountFromStorage } from "../../stores/account";
import { setLocale } from "../../stores/locale";
import type { LanguageSelectKey } from "../../types/components/select";
import debug from "../../utils/debug";
import { GM_fetch, isProxyOnlyExtension, isSupportGMXhr } from "../../utils/gm";
@ -103,6 +104,10 @@ export async function init(this: VideoHandler) {
}
await updateAccountFromStorage();
setLocale({
updatedAt: this.data.localeUpdatedAt,
hash: this.data.localeHash,
});
try {
if (