pl-fe: Allow adjusting interface size

Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
Nicole Mikołajczyk
2025-05-22 17:55:57 +02:00
parent b2c7028e27
commit bc675bbf49
7 changed files with 114 additions and 9 deletions

View File

@ -59,12 +59,15 @@ const Avatar = (props: IAvatar) => {
}).catch(() => setColor(undefined));
}, [src, isCat]);
const style: React.CSSProperties = React.useMemo(() => ({
width: size,
height: size,
fontSize: size,
color,
}), [size, color]);
const style: React.CSSProperties = React.useMemo(() => {
const value = `${size / 16}rem`;
return {
width: value,
height: value,
fontSize: value,
color,
};
}, [size, color]);
if (disableUserProvidedMedia) {
if (isAvatarMissing || !alt || isDefaultAvatar(src)) return null;

View File

@ -0,0 +1,81 @@
import throttle from 'lodash/throttle';
import React, { useRef } from 'react';
import { getPointerPosition } from 'pl-fe/features/video';
interface IStepSlider {
/** Value between 0 and the amount of steps minus one. */
value: number;
/** Steps available in the slider. */
steps: number;
/** Callback when the value changes. */
onChange(value: number): void;
}
/** Slider allowing selecting integers in a given range. */
const StepSlider: React.FC<IStepSlider> = ({ value, steps, onChange }) => {
const node = useRef<HTMLDivElement>(null);
const handleMouseDown: React.MouseEventHandler = e => {
document.addEventListener('mousemove', handleMouseSlide, true);
document.addEventListener('mouseup', handleMouseUp, true);
document.addEventListener('touchmove', handleMouseSlide, true);
document.addEventListener('touchend', handleMouseUp, true);
handleMouseSlide(e);
e.preventDefault();
e.stopPropagation();
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseSlide, true);
document.removeEventListener('mouseup', handleMouseUp, true);
document.removeEventListener('touchmove', handleMouseSlide, true);
document.removeEventListener('touchend', handleMouseUp, true);
};
const handleMouseSlide = throttle(e => {
if (node.current) {
const { x } = getPointerPosition(node.current, e);
if (!isNaN(x)) {
let slideamt = x;
if (x > 1) {
slideamt = 1;
} else if (x < 0) {
slideamt = 0;
}
slideamt = Math.floor((slideamt + 0.5 / steps) * (steps - 1));
onChange(slideamt);
}
}
}, 60);
return (
<div
className='relative inline-flex h-6 cursor-pointer transition'
onMouseDown={handleMouseDown}
ref={node}
>
<div className='absolute top-1/2 h-1 w-full -translate-y-1/2 rounded-full bg-primary-200 dark:bg-primary-700' />
<div className='absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-accent-500' style={{ width: `${value / (steps - 1) * 100}%` }} />
{[...Array(steps).fill(undefined)].map((_, step) => (
<span
key={step}
className='absolute top-1/2 z-10 h-3 w-1 -translate-y-1/2 bg-accent-300'
style={{ left: `${(step) / (steps - 1) * 100}%` }}
/>
))}
<span
className='absolute top-1/2 z-10 -ml-1.5 size-3 -translate-y-1/2 rounded-full bg-accent-500 shadow'
tabIndex={0}
style={{ left: `${value / (steps - 1) * 100}%` }}
/>
</div>
);
};
export { StepSlider as default };

View File

@ -7,6 +7,7 @@ import List, { ListItem } from 'pl-fe/components/list';
import Button from 'pl-fe/components/ui/button';
import Form from 'pl-fe/components/ui/form';
import HStack from 'pl-fe/components/ui/hstack';
import StepSlider from 'pl-fe/components/ui/step-slider';
import { Mutliselect, SelectDropdown } from 'pl-fe/features/forms';
import SettingToggle from 'pl-fe/features/settings/components/setting-toggle';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
@ -90,6 +91,8 @@ const languages = {
type Language = keyof typeof languages;
const INTERFACE_SIZES = ['sm', 'md', 'lg', 'xl'] as const;
const messages = defineMessages({
heading: { id: 'column.preferences', defaultMessage: 'Preferences' },
displayPostsDefault: { id: 'preferences.fields.display_media.default', defaultMessage: 'Hide posts marked as sensitive' },
@ -139,6 +142,11 @@ const Preferences = () => {
debouncedSave(dispatch);
};
const onInterfaceSizeChange = (value: number) => {
dispatch(changeSetting(['theme', 'interfaceSize'], INTERFACE_SIZES[value], { showAlert: true, save: false }));
debouncedSave(dispatch);
};
const onThemeReset = () => {
dispatch(changeSetting(['themeMode'], plFeConfig.defaultSettings.themeMode, { save: false }));
dispatch(changeSetting(['theme', 'brandColor'], undefined, { showAlert: true }));
@ -192,6 +200,11 @@ const Preferences = () => {
onChange={(palette) => onBrandColorChange(palette['500'])}
allowTintChange={false}
/>
<ListItem label={<div className='whitespace-nowrap'><FormattedMessage id='preferences.fields.interface_size' defaultMessage='Interface size' /></div>}>
<div className='flex w-full flex-col'>
<StepSlider value={INTERFACE_SIZES.indexOf(settings.theme?.interfaceSize || 'md')} steps={4} onChange={onInterfaceSizeChange} />
</div>
</ListItem>
</List>
<HStack justifyContent='end'>

View File

@ -15,7 +15,7 @@ const Helmet = React.lazy(() => import('pl-fe/components/helmet'));
const PlFeHead = () => {
const locale = useLocale();
const direction = useLocaleDirection(locale);
const { reduceMotion, underlineLinks, demetricator, systemFont } = useSettings();
const { reduceMotion, underlineLinks, demetricator, systemFont, theme: themeSettings } = useSettings();
const plFeConfig = usePlFeConfig();
const theme = useTheme();
@ -40,7 +40,13 @@ const PlFeHead = () => {
return (
<Helmet>
<html lang={locale} className={clsx('h-full', { 'dark': theme === 'dark', 'dark black': theme === 'black' })} />
<html
lang={locale}
className={clsx('h-full', `text-${themeSettings?.interfaceSize || 'md'}`, {
'dark': theme === 'dark',
'dark black': theme === 'black',
})}
/>
<body className={bodyClass} dir={direction} />
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
{['dark', 'black'].includes(theme) && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}

View File

@ -1345,6 +1345,7 @@
"preferences.fields.display_media.hide_all": "Always hide media posts",
"preferences.fields.display_media.show_all": "Always show posts",
"preferences.fields.implicit_addressing_label": "Include mentions in post content when replying",
"preferences.fields.interface_size": "Interface size",
"preferences.fields.known_languages_label": "Languages you know",
"preferences.fields.language_label": "Display language",
"preferences.fields.media_display_label": "Sensitive content",

View File

@ -254,7 +254,7 @@ interface IPaletteListItem {
/** Palette editor inside a ListItem. */
const PaletteListItem: React.FC<IPaletteListItem> = ({ label, palette, onChange, resetKey, allowTintChange }) => typeof palette === 'string' ? null : (
<ListItem label={<div className='w-20'>{label}</div>}>
<ListItem label={<div className='whitespace-nowrap'>{label}</div>}>
<Palette palette={palette} onChange={onChange} resetKey={resetKey} allowTintChange={allowTintChange} />
</ListItem>
);

View File

@ -55,6 +55,7 @@ const settingsSchema = v.object({
brandColor: v.fallback(v.string(), ''),
accentColor: v.fallback(v.string(), ''),
colors: v.any(),
interfaceSize: v.fallback(v.picklist(['sm', 'md', 'lg', 'xl']), 'md'),
})), undefined),
systemFont: v.fallback(v.boolean(), false),