Files
ERitors-Scribe-Desktop/components/editor/UserEditorSetting.tsx
natreex cfd08e3261 Bump app version to 0.5.0 and implement offline mode support across components
- Added offline detection logic with `OfflineContext` to improve app functionality in offline scenarios.
- Integrated Tauri IPC functions to handle local tool settings and character attributes when offline.
- Refined indentation logic in `TextEditor` for better compatibility with WebKit engines.
- Removed unused `indent` property and related settings in editor components to simplify configuration.
- Updated locale files with improved translation consistency and parameterized placeholders.
2026-03-24 22:45:10 -04:00

235 lines
11 KiB
TypeScript

'use client'
import React, {ChangeEvent, useCallback, useContext, useEffect, useMemo} from 'react';
import {Baseline, CaseSensitive, Eye, Palette, Type} from 'lucide-react';
import {useTranslations} from '@/lib/i18n';
import SelectBox from "@/components/form/SelectBox";
import Button from "@/components/ui/Button";
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
interface UserEditorSettingsProps {
settings: EditorDisplaySettings;
onSettingsChange: (settings: EditorDisplaySettings) => void;
}
export interface EditorDisplaySettings {
zoomLevel: number;
lineHeight: number;
theme: 'clair' | 'sombre' | 'sépia';
fontFamily: 'lora' | 'serif' | 'sans-serif' | 'monospace';
maxWidth: number;
focusMode: boolean;
}
const zoomLabels = ['Très petit', 'Petit', 'Normal', 'Grand', 'Très grand'] as const;
const fontSizes = [14, 16, 18, 20, 22] as const;
const themes = ['clair', 'sombre', 'sépia'] as const;
function isValidFontFamily(value: string): value is EditorDisplaySettings['fontFamily'] {
return value === 'lora' || value === 'serif' || value === 'sans-serif' || value === 'monospace';
}
const defaultSettings: EditorDisplaySettings = {
zoomLevel: 3,
lineHeight: 1.5,
theme: 'sombre',
fontFamily: 'lora',
maxWidth: 768,
focusMode: false
};
export default function UserEditorSettings({settings, onSettingsChange}: UserEditorSettingsProps): React.JSX.Element {
const t = useTranslations();
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const handleSettingChange = useCallback(<K extends keyof EditorDisplaySettings>(
key: K,
value: EditorDisplaySettings[K]
): void => {
onSettingsChange({...settings, [key]: value});
}, [settings, onSettingsChange]);
const resetToDefaults = useCallback((): void => {
onSettingsChange(defaultSettings);
}, [onSettingsChange]);
const zoomOptions = useMemo((): { value: string; label: string }[] =>
zoomLabels.map((label: typeof zoomLabels[number], index: number): { value: string; label: string } => ({
value: (index + 1).toString(),
label: `${t(`userEditorSettings.zoom.${label}`)} (${fontSizes[index]}px)`
}))
, [t]);
const themeButtons = useMemo((): { key: typeof themes[number]; isActive: boolean; className: string }[] =>
themes.map((theme: typeof themes[number]): {
key: typeof themes[number];
isActive: boolean;
className: string
} => ({
key: theme,
isActive: settings.theme === theme,
className: `p-2.5 rounded-xl border capitalize transition-colors duration-150 font-medium ${
settings.theme === theme
? 'bg-secondary text-primary border-primary'
: 'bg-secondary border-secondary text-muted hover:text-text-primary'
}`
}))
, [settings.theme]);
useEffect((): void => {
try {
const savedSettings: string | null = localStorage.getItem('userEditorSettings');
if (savedSettings) {
const parsed: Partial<EditorDisplaySettings> = JSON.parse(savedSettings);
if (parsed && typeof parsed === 'object') {
onSettingsChange({...defaultSettings, ...parsed});
}
}
} catch (e: unknown) {
onSettingsChange(defaultSettings);
}
}, [onSettingsChange]);
useEffect((): () => void => {
const timeoutId: ReturnType<typeof setTimeout> = setTimeout((): void => {
try {
localStorage.setItem('userEditorSettings', JSON.stringify(settings));
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('userEditorSettings.saveError'));
} else {
errorMessage(t('userEditorSettings.unknownError'));
}
}
}, 100);
return (): void => clearTimeout(timeoutId);
}, [settings]);
return (
<div
className="p-5 h-full overflow-y-auto">
<div className="flex items-center gap-3 mb-8 pb-4 border-b border-secondary">
<Eye className="text-primary w-6 h-6" strokeWidth={1.75}/>
<h3 className="text-xl font-['ADLaM_Display'] text-text-primary">{t("userEditorSettings.displayPreferences")}</h3>
</div>
<div className="space-y-6">
<div>
<label className="flex items-center gap-2 mb-2 text-text-primary">
<CaseSensitive className="text-muted w-5 h-5" strokeWidth={1.75}/>
{t("userEditorSettings.textSize")}
</label>
<SelectBox
defaultValue={settings.zoomLevel.toString()}
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => {
handleSettingChange('zoomLevel', Number(e.target.value))
}}
data={zoomOptions}
/>
</div>
<div>
<label className="flex items-center gap-2 mb-2 text-text-primary">
<Baseline className="text-muted w-5 h-5" strokeWidth={1.75}/>
{t("userEditorSettings.lineHeight")}
</label>
<SelectBox
defaultValue={settings.lineHeight.toString()}
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => handleSettingChange('lineHeight', Number(e.target.value))}
data={[
{value: "1.2", label: t("userEditorSettings.lineHeightCompact")},
{value: "1.5", label: t("userEditorSettings.lineHeightNormal")},
{value: "1.75", label: t("userEditorSettings.lineHeightSpaced")},
{value: "2", label: t("userEditorSettings.lineHeightDouble")}
]}
/>
</div>
<div>
<label className="flex items-center gap-2 mb-2 text-text-primary">
<Type className="text-muted w-5 h-5" strokeWidth={1.75}/>
{t("userEditorSettings.fontFamily")}
</label>
<SelectBox
defaultValue={settings.fontFamily}
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => {
const fontValue: string = e.target.value;
if (isValidFontFamily(fontValue)) {
handleSettingChange('fontFamily', fontValue);
}
}}
data={[
{value: "lora", label: t("userEditorSettings.fontLora")},
{value: "serif", label: t("userEditorSettings.fontSerif")},
{value: "sans-serif", label: t("userEditorSettings.fontSansSerif")},
{value: "monospace", label: t("userEditorSettings.fontMonospace")}
]}
/>
</div>
<div>
<label className="flex items-center gap-2 mb-2 text-text-primary">
<Baseline className="text-muted w-5 h-5" strokeWidth={1.75}/>
{t("userEditorSettings.maxWidth")}
</label>
<div className="space-y-2">
<input
type="range"
min={600}
max={1200}
step={50}
value={settings.maxWidth}
onChange={(e: ChangeEvent<HTMLInputElement>): void => handleSettingChange('maxWidth', Number(e.target.value))}
className="w-full accent-primary"
/>
<div className="flex justify-between text-sm text-muted">
<span>{t("userEditorSettings.maxWidthNarrow")}</span>
<span className="text-text-primary font-medium">{settings.maxWidth}px</span>
<span>{t("userEditorSettings.maxWidthWide")}</span>
</div>
</div>
</div>
<div>
<label className="flex items-center gap-2 mb-2 text-text-primary">
<Palette className="text-muted w-5 h-5" strokeWidth={1.75}/>
{t("userEditorSettings.theme")}
</label>
<div className="grid grid-cols-3 gap-2">
{themeButtons.map((themeBtn: {
key: typeof themes[number];
isActive: boolean;
className: string
}) => (
<button
key={themeBtn.key}
onClick={(): void => handleSettingChange('theme', themeBtn.key)}
className={themeBtn.className}
>
{t(`userEditorSettings.themeOption.${themeBtn.key}`)}
</button>
))}
</div>
</div>
<div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={settings.focusMode}
onChange={(e: ChangeEvent<HTMLInputElement>): void => handleSettingChange('focusMode', e.target.checked)}
className="w-4 h-4 accent-primary"
/>
<span className="text-text-primary">{t("userEditorSettings.focusMode")}</span>
</label>
</div>
<div className="pt-6 border-t border-secondary">
<Button variant="secondary" onClick={resetToDefaults} fullWidth>
{t("userEditorSettings.reset")}
</Button>
</div>
</div>
</div>
);
}