feat(mobile): mobile experimental feature setting (#8922)

close AF-1802

![CleanShot 2024-11-26 at 10.02.27.gif](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/09d24e35-a524-497d-b5aa-840bf74f128f.gif)
This commit is contained in:
CatsJuice
2024-11-28 07:25:06 +00:00
parent c95e6ec518
commit 71ab75e30e
10 changed files with 236 additions and 12 deletions

View File

@@ -0,0 +1,96 @@
import { Modal, Scrollable, Switch } from '@affine/component';
import { PageHeader } from '@affine/core/mobile/components';
import { useI18n } from '@affine/i18n';
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import {
AFFINE_FLAGS,
FeatureFlagService,
type Flag,
useLiveData,
useService,
} from '@toeverything/infra';
import { useCallback, useState } from 'react';
import { SettingGroup } from '../group';
import { RowLayout } from '../row.layout';
import * as styles from './styles.css';
export const ExperimentalFeatureSetting = () => {
const [open, setOpen] = useState(false);
return (
<>
<SettingGroup title="Experimental">
<RowLayout
label={'Experimental Features'}
onClick={() => setOpen(true)}
>
<ArrowRightSmallIcon fontSize={22} />
</RowLayout>
</SettingGroup>
<Modal
animation="slideRight"
open={open}
onOpenChange={setOpen}
fullScreen
contentOptions={{ className: styles.dialog }}
withoutCloseButton
>
<ExperimentalFeatureList onBack={() => setOpen(false)} />
</Modal>
</>
);
};
const ExperimentalFeatureList = ({ onBack }: { onBack: () => void }) => {
const featureFlagService = useService(FeatureFlagService);
return (
<div className={styles.root}>
<PageHeader back={!!onBack} backAction={onBack} className={styles.header}>
<span className={styles.dialogTitle}>Experimental Features</span>
</PageHeader>
<Scrollable.Root className={styles.scrollArea}>
<Scrollable.Viewport>
<ul className={styles.content}>
{Object.keys(AFFINE_FLAGS).map(key => (
<ExperimentalFeaturesItem
key={key}
flag={featureFlagService.flags[key as keyof AFFINE_FLAGS]}
/>
))}
</ul>
</Scrollable.Viewport>
<Scrollable.Scrollbar orientation="vertical" />
</Scrollable.Root>
</div>
);
};
const ExperimentalFeaturesItem = ({ flag }: { flag: Flag }) => {
const t = useI18n();
const value = useLiveData(flag.$);
const onChange = useCallback(
(checked: boolean) => {
flag.set(checked);
},
[flag]
);
if (flag.configurable === false || flag.hide) {
return null;
}
return (
<li>
<div className={styles.itemBlock}>
{t[flag.displayName]()}
<Switch checked={value} onChange={onChange} />
</div>
{flag.description ? (
<div className={styles.itemDescription}>{t[flag.description]()}</div>
) : null}
</li>
);
};

View File

@@ -0,0 +1,52 @@
import {
bodyEmphasized,
bodyRegular,
footnoteRegular,
} from '@toeverything/theme/typography';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const dialog = style({
padding: '0 !important',
background: cssVarV2('layer/background/mobile/primary'),
});
export const root = style({
display: 'flex',
flexDirection: 'column',
height: '100dvh',
});
export const header = style({
background: `${cssVarV2('layer/background/mobile/primary')} !important`,
});
export const dialogTitle = style([bodyEmphasized, {}]);
export const scrollArea = style({
height: 0,
flex: 1,
});
export const content = style({
padding: '24px 16px',
display: 'flex',
flexDirection: 'column',
gap: 16,
});
// item
export const itemBlock = style([
bodyRegular,
{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '19px 12px',
background: cssVarV2('layer/background/mobile/secondary'),
borderRadius: 12,
},
]);
export const itemDescription = style([
footnoteRegular,
{
marginTop: 4,
color: cssVarV2('text/tertiary'),
},
]);

View File

@@ -10,6 +10,7 @@ import { useEffect } from 'react';
import { AboutGroup } from './about';
import { AppearanceGroup } from './appearance';
import { ExperimentalFeatureSetting } from './experimental';
import { OthersGroup } from './others';
import * as styles from './style.css';
import { UserProfile } from './user-profile';
@@ -25,6 +26,7 @@ const MobileSetting = () => {
<UserUsage />
<AppearanceGroup />
<AboutGroup />
<ExperimentalFeatureSetting />
<OthersGroup />
</div>
);

View File

@@ -8,11 +8,17 @@ export const RowLayout = ({
label,
children,
href,
}: PropsWithChildren<{ label: ReactNode; href?: string }>) => {
onClick,
}: PropsWithChildren<{
label: ReactNode;
href?: string;
onClick?: () => void;
}>) => {
const content = (
<ConfigModal.Row
data-testid="setting-row"
className={styles.baseSettingItem}
onClick={onClick}
>
<div className={styles.baseSettingItemName}>{label}</div>
<div className={styles.baseSettingItemAction}>

View File

@@ -35,6 +35,8 @@ export const baseSettingItemAction = style([
textOverflow: 'ellipsis',
overflow: 'hidden',
flexShrink: 1,
display: 'flex',
alignItems: 'center',
},
]);