feat(electron): add tray menu settings (#11437)

fix AF-2447
This commit is contained in:
pengx17
2025-04-03 15:56:52 +00:00
parent 0aeb3041b5
commit 8ce10e6d0a
19 changed files with 330 additions and 150 deletions

View File

@@ -6,6 +6,7 @@ import {
SettingWrapper,
} from '@affine/component/setting-components';
import { LanguageMenu } from '@affine/core/components/affine/language-menu';
import { TraySettingService } from '@affine/core/modules/editor-setting/services/tray-settings';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
@@ -58,6 +59,28 @@ export const ThemeSettings = () => {
);
};
const MenubarSetting = () => {
const t = useI18n();
const traySettingService = useService(TraySettingService);
const { enabled } = useLiveData(traySettingService.setting$);
return (
<SettingWrapper
id="menubar"
title={t['com.affine.appearanceSettings.menubar.title']()}
>
<SettingRow
name={t['com.affine.appearanceSettings.menubar.toggle']()}
desc={t['com.affine.appearanceSettings.menubar.description']()}
>
<Switch
checked={enabled}
onChange={checked => traySettingService.setEnabled(checked)}
/>
</SettingRow>
</SettingWrapper>
);
};
export const AppearanceSettings = () => {
const t = useI18n();
@@ -150,6 +173,8 @@ export const AppearanceSettings = () => {
)}
</SettingWrapper>
) : null}
{BUILD_CONFIG.isElectron ? <MenubarSetting /> : null}
</>
);
};

View File

@@ -143,13 +143,11 @@ export const useGeneralSettingList = (): GeneralSettingList => {
interface GeneralSettingProps {
activeTab: SettingTab;
scrollAnchor?: string;
onChangeSettingState: (settingState: SettingState) => void;
}
export const GeneralSetting = ({
activeTab,
scrollAnchor,
onChangeSettingState,
}: GeneralSettingProps) => {
switch (activeTab) {
@@ -166,7 +164,7 @@ export const GeneralSetting = ({
case 'about':
return <AboutAffine />;
case 'plans':
return <AFFiNEPricingPlans scrollAnchor={scrollAnchor} />;
return <AFFiNEPricingPlans />;
case 'billing':
return <BillingSettings onChangeSettingState={onChangeSettingState} />;
case 'experimental-features':

View File

@@ -11,7 +11,7 @@ import { CloudPlanLayout, PlanLayout } from './layout';
import { PlansSkeleton } from './skeleton';
import * as styles from './style.css';
const Settings = ({ scrollAnchor }: { scrollAnchor?: string }) => {
const Settings = () => {
const subscriptionService = useService(SubscriptionService);
const prices = useLiveData(subscriptionService.prices.prices$);
@@ -24,23 +24,13 @@ const Settings = ({ scrollAnchor }: { scrollAnchor?: string }) => {
return <PlansSkeleton />;
}
return (
<PlanLayout
cloud={<CloudPlans />}
ai={<AIPlan />}
scrollAnchor={scrollAnchor}
/>
);
return <PlanLayout cloud={<CloudPlans />} ai={<AIPlan />} />;
};
export const AFFiNEPricingPlans = ({
scrollAnchor,
}: {
scrollAnchor?: string;
}) => {
export const AFFiNEPricingPlans = () => {
return (
<SWRErrorBoundary FallbackComponent={PlansErrorBoundary}>
<Settings scrollAnchor={scrollAnchor} />
<Settings />
</SWRErrorBoundary>
);
};

View File

@@ -8,11 +8,9 @@ import {
type HtmlHTMLAttributes,
type ReactNode,
useCallback,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { flushSync } from 'react-dom';
import * as styles from './layout.css';
@@ -69,24 +67,12 @@ export const PricingCollapsible = ({
export interface PlanLayoutProps {
cloud?: ReactNode;
ai?: ReactNode;
scrollAnchor?: string;
}
export const PlanLayout = ({ cloud, ai, scrollAnchor }: PlanLayoutProps) => {
export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
const t = useI18n();
const plansRootRef = useRef<HTMLDivElement>(null);
// TODO(@catsjuice): Need a better solution to handle this situation
useLayoutEffect(() => {
if (!scrollAnchor) return;
flushSync(() => {
const target = plansRootRef.current?.querySelector(`#${scrollAnchor}`);
if (target) {
target.scrollIntoView();
}
});
}, [scrollAnchor]);
return (
<div className={styles.plansLayoutRoot} ref={plansRootRef}>
{/* TODO(@catsjuice): SettingHeader component shouldn't have margin itself */}

View File

@@ -26,6 +26,7 @@ import {
useRef,
useState,
} from 'react';
import { flushSync } from 'react-dom';
import { AccountSetting } from './account-setting';
import { GeneralSetting } from './general-setting';
@@ -39,6 +40,7 @@ import { WorkspaceSetting } from './workspace-setting';
interface SettingProps extends ModalProps {
activeTab?: SettingTab;
onCloseSetting: () => void;
scrollAnchor?: string;
}
const isWorkspaceSetting = (key: string): boolean =>
@@ -55,10 +57,11 @@ const CenteredLoading = () => {
const SettingModalInner = ({
activeTab: initialActiveTab = 'appearance',
onCloseSetting,
scrollAnchor: initialScrollAnchor,
}: SettingProps) => {
const [settingState, setSettingState] = useState<SettingState>({
activeTab: initialActiveTab,
scrollAnchor: undefined,
scrollAnchor: initialScrollAnchor,
});
const globalContextService = useService(GlobalContextService);
@@ -150,6 +153,18 @@ const SettingModalInner = ({
}
}, [isSelfhosted, settingState.activeTab]);
useEffect(() => {
if (settingState.scrollAnchor) {
flushSync(() => {
const target = modalContentRef.current?.querySelector(
`#${settingState.scrollAnchor}`
);
if (target) {
target.scrollIntoView();
}
});
}
}, [settingState]);
return (
<FrameworkScope scope={currentServer.scope}>
<SettingSidebar
@@ -165,7 +180,6 @@ const SettingModalInner = ({
<div className={style.centerContainer}>
<div ref={modalContentRef} className={style.content}>
<Suspense fallback={<WorkspaceDetailSkeleton />}>
{}
{settingState.activeTab === 'account' &&
loginStatus === 'authenticated' ? (
<AccountSetting onChangeSettingState={setSettingState} />
@@ -178,7 +192,6 @@ const SettingModalInner = ({
) : !isWorkspaceSetting(settingState.activeTab) ? (
<GeneralSetting
activeTab={settingState.activeTab}
scrollAnchor={settingState.scrollAnchor}
onChangeSettingState={setSettingState}
/>
) : null}
@@ -223,6 +236,7 @@ const SettingModalInner = ({
export const SettingDialog = ({
close,
activeTab,
scrollAnchor,
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['setting']>) => {
return (
<Modal
@@ -242,7 +256,11 @@ export const SettingDialog = ({
onOpenChange={() => close()}
>
<Suspense fallback={<CenteredLoading />}>
<SettingModalInner activeTab={activeTab} onCloseSetting={close} />
<SettingModalInner
activeTab={activeTab}
onCloseSetting={close}
scrollAnchor={scrollAnchor}
/>
</Suspense>
</Modal>
);

View File

@@ -38,4 +38,11 @@ export class WorkspaceDialogService extends Service {
})
);
}
closeAll() {
const dialogs = this.dialogs$.value;
dialogs.forEach(dialog => {
this.close(dialog.id);
});
}
}

View File

@@ -9,6 +9,7 @@ import { CurrentUserDBEditorSettingProvider } from './impls/user-db';
import { EditorSettingProvider } from './provider/editor-setting-provider';
import { EditorSettingService } from './services/editor-setting';
import { SpellCheckSettingService } from './services/spell-check-setting';
import { TraySettingService } from './services/tray-settings';
export type { FontFamily } from './schema';
export { EditorSettingSchema, fontStyleOptions } from './schema';
export { EditorSettingService } from './services/editor-setting';
@@ -30,3 +31,7 @@ export function configureSpellCheckSettingModule(framework: Framework) {
DesktopApiService,
]);
}
export function configureTraySettingModule(framework: Framework) {
framework.service(TraySettingService, [GlobalStateService]);
}

View File

@@ -0,0 +1,28 @@
import type {
MenubarStateKey,
MenubarStateSchema,
} from '@affine/electron/main/shared-state-schema';
import { LiveData, Service } from '@toeverything/infra';
import type { GlobalStateService } from '../../storage';
const MENUBAR_SETTING_KEY: typeof MenubarStateKey = 'menubarState';
export class TraySettingService extends Service {
constructor(private readonly globalStateService: GlobalStateService) {
super();
}
setting$ = LiveData.from(
this.globalStateService.globalState.watch<MenubarStateSchema>(
MENUBAR_SETTING_KEY
),
null
).map(v => v ?? { enabled: true });
setEnabled(enabled: boolean) {
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
enabled,
});
}
}