mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(mobile): mobile experimental feature setting (#8922)
close AF-1802 
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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'),
|
||||
},
|
||||
]);
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -35,6 +35,8 @@ export const baseSettingItemAction = style([
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user