Skip to content

Commit ad11007

Browse files
committed
feat(mobile): setting page ui (#8048)
AF-1275
1 parent bea3d42 commit ad11007

File tree

35 files changed

+1205
-80
lines changed

35 files changed

+1205
-80
lines changed

packages/frontend/component/src/ui/modal/modal.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export interface ModalProps extends DialogProps {
3939
* @default 'fadeScaleTop'
4040
*/
4141
animation?: 'fadeScaleTop' | 'none' | 'slideBottom';
42+
/**
43+
* Whether to show the modal in full screen mode
44+
*/
45+
fullScreen?: boolean;
4246
}
4347
type PointerDownOutsideEvent = Parameters<
4448
Exclude<DialogContentProps['onPointerDownOutside'], undefined>
@@ -144,6 +148,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
144148
animation = environment.isBrowser && environment.isMobile
145149
? 'slideBottom'
146150
: 'fadeScaleTop',
151+
fullScreen,
147152
...otherProps
148153
} = props;
149154
const { className: closeButtonClassName, ...otherCloseButtonProps } =
@@ -209,6 +214,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
209214
{...otherOverlayOptions}
210215
/>
211216
<div
217+
data-full-screen={fullScreen}
212218
data-modal={modal}
213219
className={clsx(
214220
`anim-${animation}`,
@@ -223,8 +229,14 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
223229
className={clsx(styles.modalContent, contentClassName)}
224230
style={{
225231
...assignInlineVars({
226-
[styles.widthVar]: getVar(width, '50vw'),
227-
[styles.heightVar]: getVar(height, 'unset'),
232+
[styles.widthVar]: getVar(
233+
width,
234+
fullScreen ? '100dvw' : '50dvw'
235+
),
236+
[styles.heightVar]: getVar(
237+
height,
238+
fullScreen ? '100dvh' : 'unset'
239+
),
228240
[styles.minHeightVar]: getVar(minHeight, '26px'),
229241
}),
230242
...contentStyle,

packages/frontend/component/src/ui/modal/styles.css.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ export const modalContentWrapper = style({
8484
},
8585

8686
selectors: {
87+
'&[data-full-screen="true"]': {
88+
padding: '0 !important',
89+
},
8790
'&.anim-none': {
8891
animation: 'none',
8992
},
@@ -136,6 +139,19 @@ export const modalContent = style({
136139
borderRadius: '12px',
137140
// :focus-visible will set outline
138141
outline: 'none',
142+
143+
selectors: {
144+
'[data-full-screen="true"] &': {
145+
vars: {
146+
[widthVar]: '100vw',
147+
[heightVar]: '100vh',
148+
[minHeightVar]: '100vh',
149+
},
150+
maxWidth: '100vw',
151+
maxHeight: '100vh',
152+
borderRadius: 0,
153+
},
154+
},
139155
});
140156
export const closeButton = style({
141157
position: 'absolute',

packages/frontend/core/src/atoms/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import type { SettingProps } from '../components/affine/setting-modal';
55
import type { ActiveTab } from '../components/affine/setting-modal/types';
66
// modal atoms
77
export const openWorkspacesModalAtom = atom(false);
8+
/**
9+
* @deprecated use `useSignOut` hook instated
10+
*/
811
export const openSignOutModalAtom = atom(false);
912
export const openQuotaModalAtom = atom(false);
1013
export const openStarAFFiNEModalAtom = atom(false);

packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,30 @@ import { DateFormatSetting } from './date-format-setting';
1717
import { settingWrapper } from './style.css';
1818
import { ThemeEditorSetting } from './theme-editor-setting';
1919

20+
export const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
21+
[
22+
{
23+
value: 'system',
24+
label: t['com.affine.themeSettings.system'](),
25+
testId: 'system-theme-trigger',
26+
},
27+
{
28+
value: 'light',
29+
label: t['com.affine.themeSettings.light'](),
30+
testId: 'light-theme-trigger',
31+
},
32+
{
33+
value: 'dark',
34+
label: t['com.affine.themeSettings.dark'](),
35+
testId: 'dark-theme-trigger',
36+
},
37+
] satisfies RadioItem[];
38+
2039
export const ThemeSettings = () => {
2140
const t = useI18n();
2241
const { setTheme, theme } = useTheme();
2342

24-
const radioItems = useMemo<RadioItem[]>(
25-
() => [
26-
{
27-
value: 'system',
28-
label: t['com.affine.themeSettings.system'](),
29-
testId: 'system-theme-trigger',
30-
},
31-
{
32-
value: 'light',
33-
label: t['com.affine.themeSettings.light'](),
34-
testId: 'light-theme-trigger',
35-
},
36-
{
37-
value: 'dark',
38-
label: t['com.affine.themeSettings.dark'](),
39-
testId: 'dark-theme-trigger',
40-
},
41-
],
42-
[t]
43-
);
43+
const radioItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);
4444

4545
return (
4646
<RadioGroup

packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -41,51 +41,67 @@ import { Virtuoso } from 'react-virtuoso';
4141
import { DropdownMenu } from './menu';
4242
import * as styles from './style.css';
4343

44+
const getLabel = (fontKey: FontFamily, t: ReturnType<typeof useI18n>) => {
45+
switch (fontKey) {
46+
case 'Sans':
47+
return t['com.affine.appearanceSettings.fontStyle.sans']();
48+
case 'Serif':
49+
return t['com.affine.appearanceSettings.fontStyle.serif']();
50+
case 'Mono':
51+
return t[`com.affine.appearanceSettings.fontStyle.mono`]();
52+
case 'Custom':
53+
return t['com.affine.settings.editorSettings.edgeless.custom']();
54+
default:
55+
return '';
56+
}
57+
};
58+
59+
export const getBaseFontStyleOptions = (
60+
t: ReturnType<typeof useI18n>
61+
): Array<Omit<RadioItem, 'value'> & { value: FontFamily }> => {
62+
return fontStyleOptions
63+
.map(({ key, value }) => {
64+
if (key === 'Custom') {
65+
return null;
66+
}
67+
const label = getLabel(key, t);
68+
return {
69+
value: key,
70+
label,
71+
testId: 'system-font-style-trigger',
72+
style: {
73+
fontFamily: value,
74+
},
75+
} satisfies RadioItem;
76+
})
77+
.filter(item => item !== null);
78+
};
79+
4480
const FontFamilySettings = () => {
4581
const t = useI18n();
4682
const { editorSettingService } = useServices({ EditorSettingService });
4783
const settings = useLiveData(editorSettingService.editorSetting.settings$);
4884

49-
const getLabel = useCallback(
50-
(fontKey: FontFamily) => {
51-
switch (fontKey) {
52-
case 'Sans':
53-
return t['com.affine.appearanceSettings.fontStyle.sans']();
54-
case 'Serif':
55-
return t['com.affine.appearanceSettings.fontStyle.serif']();
56-
case 'Mono':
57-
return t[`com.affine.appearanceSettings.fontStyle.mono`]();
58-
case 'Custom':
59-
return t['com.affine.settings.editorSettings.edgeless.custom']();
60-
default:
61-
return '';
62-
}
63-
},
64-
[t]
65-
);
66-
6785
const radioItems = useMemo(() => {
68-
return fontStyleOptions
69-
.map(({ key, value }) => {
70-
if (key === 'Custom' && !environment.isDesktop) {
71-
return null;
72-
}
73-
const label = getLabel(key);
74-
let fontFamily = value;
75-
if (key === 'Custom' && settings.customFontFamily) {
76-
fontFamily = `${settings.customFontFamily}, ${value}`;
77-
}
78-
return {
79-
value: key,
80-
label,
81-
testId: 'system-font-style-trigger',
82-
style: {
83-
fontFamily,
84-
},
85-
} satisfies RadioItem;
86-
})
87-
.filter(item => item !== null);
88-
}, [getLabel, settings.customFontFamily]);
86+
const items = getBaseFontStyleOptions(t);
87+
if (!environment.isDesktop) return items;
88+
89+
// resolve custom fonts
90+
const customOption = fontStyleOptions.find(opt => opt.key === 'Custom');
91+
if (customOption) {
92+
const fontFamily = settings.customFontFamily
93+
? `${settings.customFontFamily}, ${customOption.value}`
94+
: customOption.value;
95+
items.push({
96+
value: customOption.key,
97+
label: getLabel(customOption.key, t),
98+
testId: 'system-font-style-trigger',
99+
style: { fontFamily },
100+
});
101+
}
102+
103+
return items;
104+
}, [settings.customFontFamily, t]);
89105

90106
const handleFontFamilyChange = useCallback(
91107
(value: FontFamily) => {

packages/frontend/core/src/components/affine/sign-out-modal/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ type SignOutConfirmModalI18NKeys =
99
| 'cancel'
1010
| 'confirm';
1111

12+
/**
13+
* @deprecated use `useSignOut` instead
14+
*/
1215
export const SignOutModal = ({ ...props }: ConfirmModalProps) => {
1316
const { title, description, cancelText, confirmText } = props;
1417
const t = useI18n();
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
type ConfirmModalProps,
3+
notify,
4+
useConfirmModal,
5+
} from '@affine/component';
6+
import { AuthService } from '@affine/core/modules/cloud';
7+
import { WorkspaceSubPath } from '@affine/core/shared';
8+
import { WorkspaceFlavour } from '@affine/env/workspace';
9+
import { useI18n } from '@affine/i18n';
10+
import {
11+
GlobalContextService,
12+
useLiveData,
13+
useService,
14+
WorkspacesService,
15+
} from '@toeverything/infra';
16+
import { useCallback } from 'react';
17+
18+
import { useNavigateHelper } from '../use-navigate-helper';
19+
20+
type SignOutConfirmModalI18NKeys =
21+
| 'title'
22+
| 'description'
23+
| 'cancel'
24+
| 'confirm';
25+
26+
export const useSignOut = ({
27+
onConfirm,
28+
confirmButtonOptions,
29+
contentOptions,
30+
...props
31+
}: ConfirmModalProps = {}) => {
32+
const t = useI18n();
33+
const { openConfirmModal } = useConfirmModal();
34+
const { openPage } = useNavigateHelper();
35+
36+
const authService = useService(AuthService);
37+
const workspacesService = useService(WorkspacesService);
38+
const globalContextService = useService(GlobalContextService);
39+
40+
const workspaces = useLiveData(workspacesService.list.workspaces$);
41+
const currentWorkspaceId = useLiveData(
42+
globalContextService.globalContext.workspaceId.$
43+
);
44+
const currentWorkspaceMetadata = useLiveData(
45+
currentWorkspaceId
46+
? workspacesService.list.workspace$(currentWorkspaceId)
47+
: undefined
48+
);
49+
50+
const signOut = useCallback(async () => {
51+
onConfirm?.()?.catch(console.error);
52+
try {
53+
await authService.signOut();
54+
} catch (err) {
55+
console.error(err);
56+
// TODO(@eyhn): i18n
57+
notify.error({
58+
title: 'Failed to sign out',
59+
});
60+
}
61+
62+
// if current workspace is affine cloud, switch to local workspace
63+
if (currentWorkspaceMetadata?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
64+
const localWorkspace = workspaces.find(
65+
w => w.flavour === WorkspaceFlavour.LOCAL
66+
);
67+
if (localWorkspace) {
68+
openPage(localWorkspace.id, WorkspaceSubPath.ALL);
69+
}
70+
}
71+
}, [
72+
authService,
73+
currentWorkspaceMetadata?.flavour,
74+
onConfirm,
75+
openPage,
76+
workspaces,
77+
]);
78+
79+
const getDefaultText = useCallback(
80+
(key: SignOutConfirmModalI18NKeys) => {
81+
return t[`com.affine.auth.sign-out.confirm-modal.${key}`]();
82+
},
83+
[t]
84+
);
85+
86+
const confirmSignOut = useCallback(() => {
87+
openConfirmModal({
88+
title: getDefaultText('title'),
89+
description: getDefaultText('description'),
90+
cancelText: getDefaultText('cancel'),
91+
confirmText: getDefaultText('confirm'),
92+
confirmButtonOptions: {
93+
...confirmButtonOptions,
94+
variant: 'error',
95+
['data-testid' as string]: 'confirm-sign-out-button',
96+
},
97+
contentOptions: {
98+
...contentOptions,
99+
['data-testid' as string]: 'confirm-sign-out-modal',
100+
},
101+
onConfirm: signOut,
102+
...props,
103+
});
104+
}, [
105+
confirmButtonOptions,
106+
contentOptions,
107+
getDefaultText,
108+
openConfirmModal,
109+
props,
110+
signOut,
111+
]);
112+
113+
return confirmSignOut;
114+
};

packages/frontend/i18n/src/resources/en.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,5 +1567,18 @@
15671567
"com.affine.import-template.dialog.createDocToWorkspace": "Create doc to \"{{workspace}}\"",
15681568
"com.affine.import-template.dialog.createDocToNewWorkspace": "Create into a New Workspace",
15691569
"com.affine.import-template.dialog.createDocWithTemplate": "Create doc with \"{{templateName}}\" template",
1570+
"com.affine.mobile.setting.header-title": "Settings",
1571+
"com.affine.mobile.setting.appearance.title": "Appearance",
1572+
"com.affine.mobile.setting.appearance.theme": "Color mode",
1573+
"com.affine.mobile.setting.appearance.font": "Font style",
1574+
"com.affine.mobile.setting.appearance.language": "Display language",
1575+
"com.affine.mobile.setting.about.title": "About",
1576+
"com.affine.mobile.setting.about.appVersion": "App version",
1577+
"com.affine.mobile.setting.about.editorVersion": "Editor version",
1578+
"com.affine.mobile.setting.others.title": "Privacy & others",
1579+
"com.affine.mobile.setting.others.github": "Star us on GitHub",
1580+
"com.affine.mobile.setting.others.website": "Official website",
1581+
"com.affine.mobile.setting.others.privacy": "Privacy",
1582+
"com.affine.mobile.setting.others.terms": "Terms of use",
15701583
"com.affine.mobile.search.empty": "No results found"
15711584
}

packages/frontend/mobile/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
"core-js": "^3.36.1",
2121
"figma-squircle": "^0.3.1",
2222
"intl-segmenter-polyfill-rs": "^0.1.7",
23+
"jotai": "^2.9.3",
24+
"jotai-devtools": "^0.10.1",
25+
"jotai-effect": "^1.0.2",
26+
"jotai-scope": "^0.7.2",
2327
"lodash-es": "^4.17.21",
28+
"next-themes": "^0.3.0",
2429
"react": "^18.2.0",
2530
"react-dom": "^18.2.0",
2631
"react-router-dom": "^6.26.1"

0 commit comments

Comments
 (0)