mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 18:46:19 +08:00
refactor(core): desktop project struct (#8334)
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { Button, FlexWrapper, notify } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AiIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import Lottie from 'lottie-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
@@ -51,24 +50,20 @@ export const AIOnboardingEdgeless = () => {
|
||||
const notifyId = useLiveData(edgelessNotifyId$);
|
||||
const generalAIOnboardingOpened = useLiveData(showAIOnboardingGeneral$);
|
||||
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const settingModalOpen = useAtomValue(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const mode = useLiveData(editorService.editor.mode$);
|
||||
|
||||
const goToPricingPlans = useCallback(() => {
|
||||
track.$.aiOnboarding.dialog.viewPlans();
|
||||
setSettingModal({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'aiPricingPlan',
|
||||
});
|
||||
}, [setSettingModal]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settingModalOpen.open) return;
|
||||
if (generalAIOnboardingOpened) return;
|
||||
if (notifyId) return;
|
||||
if (mode !== 'edgeless') return;
|
||||
@@ -128,7 +123,6 @@ export const AIOnboardingEdgeless = () => {
|
||||
goToPricingPlans,
|
||||
mode,
|
||||
notifyId,
|
||||
settingModalOpen,
|
||||
t,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Button, IconButton, Modal } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { useBlurRoot } from '@affine/core/components/hooks/use-blur-root';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
@@ -96,8 +95,8 @@ export const AIOnboardingGeneral = () => {
|
||||
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const [index, setIndex] = useState(0);
|
||||
const list = useMemo(() => getPlayList(t), [t]);
|
||||
const [settingModal, setSettingModal] = useAtom(openSettingModalAtom);
|
||||
const readyToOpen = isLoggedIn && !settingModal.open;
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const readyToOpen = isLoggedIn;
|
||||
useBlurRoot(open && readyToOpen);
|
||||
|
||||
const isFirst = index === 0;
|
||||
@@ -111,14 +110,13 @@ export const AIOnboardingGeneral = () => {
|
||||
toggleGeneralAIOnboarding(false);
|
||||
}, []);
|
||||
const goToPricingPlans = useCallback(() => {
|
||||
setSettingModal({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'aiPricingPlan',
|
||||
});
|
||||
track.$.aiOnboarding.dialog.viewPlans();
|
||||
closeAndDismiss();
|
||||
}, [closeAndDismiss, setSettingModal]);
|
||||
}, [closeAndDismiss, globalDialogService]);
|
||||
const onPrev = useCallback(() => {
|
||||
setIndex(i => Math.max(0, i - 1));
|
||||
}, []);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const electronFallback = style({
|
||||
paddingTop: 52,
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
AppSidebarFallback,
|
||||
ShellAppSidebarFallback,
|
||||
} from '@affine/core/modules/app-sidebar/views';
|
||||
import clsx from 'clsx';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../components/hooks/affine/use-app-setting-helper';
|
||||
import type { WorkspaceRootProps } from '../workspace';
|
||||
import {
|
||||
AppContainer as AppContainerWithoutSettings,
|
||||
MainContainerFallback,
|
||||
} from '../workspace';
|
||||
import * as styles from './app-container.css';
|
||||
|
||||
export const AppContainer = (props: WorkspaceRootProps) => {
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
return (
|
||||
<AppContainerWithoutSettings
|
||||
useNoisyBackground={appSettings.enableNoisyBackground}
|
||||
useBlurBackground={appSettings.enableBlurBackground}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppFallback = ({
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
className?: string;
|
||||
}>): ReactElement => {
|
||||
return (
|
||||
<AppContainer
|
||||
className={clsx(
|
||||
className,
|
||||
BUILD_CONFIG.isElectron && styles.electronFallback
|
||||
)}
|
||||
>
|
||||
<AppSidebarFallback />
|
||||
<MainContainerFallback>{children}</MainContainerFallback>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShellAppFallback = ({
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
className?: string;
|
||||
}>): ReactElement => {
|
||||
return (
|
||||
<AppContainer className={className}>
|
||||
<ShellAppSidebarFallback />
|
||||
<MainContainerFallback>{children}</MainContainerFallback>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
} from '../../../modules/cloud';
|
||||
import { openSettingModalAtom } from '../../atoms';
|
||||
import * as styles from './style.css';
|
||||
|
||||
export const UserPlanButton = () => {
|
||||
@@ -35,14 +34,13 @@ export const UserPlanButton = () => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const handleClick = useCatchEventCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { AllDocsIcon, FilterIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useEditCollection } from '../../page-list';
|
||||
import { ActionButton } from './action-button';
|
||||
import collectionDetailDark from './assets/collection-detail.dark.png';
|
||||
import collectionDetailLight from './assets/collection-detail.light.png';
|
||||
@@ -41,18 +40,21 @@ export const EmptyCollectionDetail = ({
|
||||
|
||||
const Actions = ({ collection }: { collection: Collection }) => {
|
||||
const t = useI18n();
|
||||
const collectionService = useService(CollectionService);
|
||||
const { open } = useEditCollection();
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
|
||||
const openAddDocs = useAsyncCallback(async () => {
|
||||
const ret = await open({ ...collection }, 'page');
|
||||
collectionService.updateCollection(ret.id, () => ret);
|
||||
}, [open, collection, collectionService]);
|
||||
const openAddDocs = useCallback(() => {
|
||||
workspaceDialogService.open('collection-editor', {
|
||||
collectionId: collection.id,
|
||||
mode: 'page',
|
||||
});
|
||||
}, [collection, workspaceDialogService]);
|
||||
|
||||
const openAddRules = useAsyncCallback(async () => {
|
||||
const ret = await open({ ...collection }, 'rule');
|
||||
collectionService.updateCollection(ret.id, () => ret);
|
||||
}, [collection, open, collectionService]);
|
||||
const openAddRules = useCallback(() => {
|
||||
workspaceDialogService.open('collection-editor', {
|
||||
collectionId: collection.id,
|
||||
mode: 'rule',
|
||||
});
|
||||
}, [collection, workspaceDialogService]);
|
||||
|
||||
return (
|
||||
<div className={actionGroup}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { usePromptModal } from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -6,7 +7,7 @@ import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { createEmptyCollection, useEditCollectionName } from '../../page-list';
|
||||
import { createEmptyCollection } from '../../page-list';
|
||||
import { ActionButton } from './action-button';
|
||||
import collectionListDark from './assets/collection-list.dark.png';
|
||||
import collectionListLight from './assets/collection-list.light.png';
|
||||
@@ -19,24 +20,36 @@ export const EmptyCollections = (props: UniversalEmptyProps) => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const { open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.createCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
const { openPromptModal } = usePromptModal();
|
||||
|
||||
const showAction = true;
|
||||
|
||||
const handleCreateCollection = useCallback(() => {
|
||||
open('')
|
||||
.then(name => {
|
||||
openPromptModal({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
label: t['com.affine.editCollectionName.name'](),
|
||||
inputOptions: {
|
||||
placeholder: t['com.affine.editCollectionName.name.placeholder'](),
|
||||
},
|
||||
children: t['com.affine.editCollectionName.createTips'](),
|
||||
confirmText: t['com.affine.editCollection.save'](),
|
||||
cancelText: t['com.affine.editCollection.button.cancel'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
const id = nanoid();
|
||||
collectionService.addCollection(createEmptyCollection(id, { name }));
|
||||
navigateHelper.jumpToCollection(currentWorkspace.id, id);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [collectionService, currentWorkspace, navigateHelper, open]);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
collectionService,
|
||||
currentWorkspace.id,
|
||||
navigateHelper,
|
||||
openPromptModal,
|
||||
t,
|
||||
]);
|
||||
|
||||
return (
|
||||
<EmptyLayout
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { HelpIsland } from '../../pure/help-island';
|
||||
import { ToolContainer } from '../../workspace';
|
||||
|
||||
export const HubIsland = () => {
|
||||
return (
|
||||
<ToolContainer>
|
||||
<HelpIsland />
|
||||
</ToolContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import { OverlayModal } from '@affine/component';
|
||||
import { openIssueFeedbackModalAtom } from '@affine/core/components/atoms';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
export const IssueFeedbackModal = () => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useAtom(openIssueFeedbackModalAtom);
|
||||
|
||||
return (
|
||||
<OverlayModal
|
||||
open={open}
|
||||
topImage={
|
||||
<video
|
||||
width={400}
|
||||
height={300}
|
||||
style={{ objectFit: 'cover' }}
|
||||
src={'/static/newIssue.mp4'}
|
||||
autoPlay
|
||||
loop
|
||||
/>
|
||||
}
|
||||
title={t['com.affine.issue-feedback.title']()}
|
||||
onOpenChange={setOpen}
|
||||
description={t['com.affine.issue-feedback.description']()}
|
||||
cancelText={t['com.affine.issue-feedback.cancel']()}
|
||||
to={`${BUILD_CONFIG.githubUrl}/issues/new/choose`}
|
||||
confirmText={t['com.affine.issue-feedback.confirm']()}
|
||||
confirmButtonOptions={{
|
||||
variant: 'primary',
|
||||
}}
|
||||
external
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import { Loading, Scrollable } from '@affine/component';
|
||||
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
||||
import { Button, IconButton } from '@affine/component/ui/button';
|
||||
import { Modal, useConfirmModal } from '@affine/component/ui/modal';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
@@ -18,7 +18,7 @@ import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons/rc';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
@@ -188,21 +188,19 @@ const PlanPrompt = () => {
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const [planPromptClosed, setPlanPromptClosed] = useAtom(planPromptClosedAtom);
|
||||
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const closeFreePlanPrompt = useCallback(() => {
|
||||
setPlanPromptClosed(true);
|
||||
}, [setPlanPromptClosed]);
|
||||
|
||||
const onClickUpgrade = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
track.$.docHistory.$.viewPlans();
|
||||
}, [setSettingModalAtom]);
|
||||
}, [globalDialogService]);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
|
||||
+6
-9
@@ -1,16 +1,14 @@
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import {
|
||||
openQuotaModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { openQuotaModalAtom } from '@affine/core/components/atoms';
|
||||
import { UserQuotaService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import bytes from 'bytes';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
export const CloudQuotaModal = () => {
|
||||
@@ -45,17 +43,16 @@ export const CloudQuotaModal = () => {
|
||||
return isOwner && userQuota?.name === 'free';
|
||||
}, [isOwner, userQuota]);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const handleUpgradeConfirm = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
|
||||
track.$.paywall.storage.viewPlans();
|
||||
setOpen(false);
|
||||
}, [setOpen, setSettingModalAtom]);
|
||||
}, [globalDialogService, setOpen]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (userQuota && isFreePlanOwner) {
|
||||
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
import { Button, ErrorMessage, Skeleton } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import {
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
UserCopilotQuotaService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { AIResume, AISubscribe } from '../general-setting/plans/ai/actions';
|
||||
import * as styles from './storage-progress.css';
|
||||
|
||||
export const AIUsagePanel = () => {
|
||||
const t = useI18n();
|
||||
const setOpenSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const serverConfigService = useService(ServerConfigService);
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
||||
);
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
useEffect(() => {
|
||||
// revalidate latest subscription status
|
||||
subscriptionService.subscription.revalidate();
|
||||
}, [subscriptionService]);
|
||||
const copilotQuotaService = useService(UserCopilotQuotaService);
|
||||
useEffect(() => {
|
||||
copilotQuotaService.copilotQuota.revalidate();
|
||||
}, [copilotQuotaService]);
|
||||
const copilotActionLimit = useLiveData(
|
||||
copilotQuotaService.copilotQuota.copilotActionLimit$
|
||||
);
|
||||
const copilotActionUsed = useLiveData(
|
||||
copilotQuotaService.copilotQuota.copilotActionUsed$
|
||||
);
|
||||
const loading = copilotActionLimit === null || copilotActionUsed === null;
|
||||
const loadError = useLiveData(copilotQuotaService.copilotQuota.error$);
|
||||
|
||||
const openBilling = useCallback(() => {
|
||||
setOpenSettingModal({
|
||||
open: true,
|
||||
activeTab: 'billing',
|
||||
});
|
||||
track.$.settingsPanel.accountUsage.viewPlans({ plan: SubscriptionPlan.AI });
|
||||
}, [setOpenSettingModal]);
|
||||
|
||||
if (loading) {
|
||||
if (loadError) {
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.ai.usage-title']()}
|
||||
desc={''}
|
||||
spreadCol={false}
|
||||
>
|
||||
{/* TODO(@catsjuice): i18n */}
|
||||
<ErrorMessage>Load error</ErrorMessage>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.ai.usage-title']()}
|
||||
desc={''}
|
||||
spreadCol={false}
|
||||
>
|
||||
<Skeleton height={42} />
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
const percent =
|
||||
copilotActionLimit === 'unlimited'
|
||||
? 0
|
||||
: Math.min(
|
||||
100,
|
||||
Math.max(
|
||||
0.5,
|
||||
Number(((copilotActionUsed / copilotActionLimit) * 100).toFixed(4))
|
||||
)
|
||||
);
|
||||
|
||||
const color = percent > 80 ? cssVar('errorColor') : cssVar('processingColor');
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
spreadCol={aiSubscription ? true : false}
|
||||
desc={
|
||||
aiSubscription
|
||||
? t['com.affine.payment.ai.usage-description-purchased']()
|
||||
: ''
|
||||
}
|
||||
name={t['com.affine.payment.ai.usage-title']()}
|
||||
>
|
||||
{copilotActionLimit === 'unlimited' ? (
|
||||
hasPaymentFeature && aiSubscription?.canceledAt ? (
|
||||
<AIResume />
|
||||
) : (
|
||||
<Button onClick={openBilling}>
|
||||
{t['com.affine.payment.ai.usage.change-button-label']()}
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<div className={styles.storageProgressContainer}>
|
||||
<div className={styles.storageProgressWrapper}>
|
||||
<div className="storage-progress-desc">
|
||||
<span>{t['com.affine.payment.ai.usage.used-caption']()}</span>
|
||||
<span>
|
||||
{t['com.affine.payment.ai.usage.used-detail']({
|
||||
used: copilotActionUsed.toString(),
|
||||
limit: copilotActionLimit.toString(),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="storage-progress-bar-wrapper">
|
||||
<div
|
||||
className={styles.storageProgressBar}
|
||||
style={{ width: `${percent}%`, backgroundColor: color }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasPaymentFeature && (
|
||||
<AISubscribe variant="primary">
|
||||
{t['com.affine.payment.ai.usage.purchase-button-label']()}
|
||||
</AISubscribe>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
@@ -1,271 +0,0 @@
|
||||
import { FlexWrapper, Input, notify } from '@affine/component';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useEnsureLiveData,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { AuthService, ServerConfigService } from '../../../../modules/cloud';
|
||||
import {
|
||||
authAtom,
|
||||
openSettingModalAtom,
|
||||
openSignOutModalAtom,
|
||||
} from '../../../atoms';
|
||||
import { Upload } from '../../../pure/file-upload';
|
||||
import { AIUsagePanel } from './ai-usage-panel';
|
||||
import { StorageProgress } from './storage-progress';
|
||||
import * as styles from './style.css';
|
||||
|
||||
export const UserAvatar = () => {
|
||||
const t = useI18n();
|
||||
const session = useService(AuthService).session;
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
|
||||
const handleUpdateUserAvatar = useAsyncCallback(
|
||||
async (file: File) => {
|
||||
try {
|
||||
track.$.settingsPanel.accountSettings.uploadAvatar();
|
||||
await session.uploadAvatar(file);
|
||||
notify.success({ title: 'Update user avatar success' });
|
||||
} catch (e) {
|
||||
// TODO(@catsjuice): i18n
|
||||
notify.error({
|
||||
title: 'Update user avatar failed',
|
||||
message: String(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
[session]
|
||||
);
|
||||
|
||||
const handleRemoveUserAvatar = useCatchEventCallback(async () => {
|
||||
track.$.settingsPanel.accountSettings.removeAvatar();
|
||||
await session.removeAvatar();
|
||||
}, [session]);
|
||||
|
||||
return (
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={handleUpdateUserAvatar}
|
||||
data-testid="upload-user-avatar"
|
||||
>
|
||||
<Avatar
|
||||
size={56}
|
||||
name={account.label}
|
||||
url={account.avatar}
|
||||
hoverIcon={<CameraIcon />}
|
||||
onRemove={account.avatar ? handleRemoveUserAvatar : undefined}
|
||||
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
|
||||
removeTooltipOptions={{ content: t['Remove photo']() }}
|
||||
data-testid="user-setting-avatar"
|
||||
removeButtonProps={{
|
||||
['data-testid' as string]: 'user-setting-remove-avatar-button',
|
||||
}}
|
||||
/>
|
||||
</Upload>
|
||||
);
|
||||
};
|
||||
|
||||
export const AvatarAndName = () => {
|
||||
const t = useI18n();
|
||||
const session = useService(AuthService).session;
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
const [input, setInput] = useState<string>(account.label);
|
||||
|
||||
const allowUpdate = !!input && input !== account.label;
|
||||
const handleUpdateUserName = useAsyncCallback(async () => {
|
||||
if (account === null) {
|
||||
return;
|
||||
}
|
||||
if (!allowUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
track.$.settingsPanel.accountSettings.updateUserName();
|
||||
await session.updateLabel(input);
|
||||
} catch (e) {
|
||||
notify.error({
|
||||
title: 'Failed to update user name.',
|
||||
message: String(e),
|
||||
});
|
||||
}
|
||||
}, [account, allowUpdate, session, input]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.profile']()}
|
||||
desc={t['com.affine.settings.profile.message']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<FlexWrapper style={{ margin: '12px 0 24px 0' }} alignItems="center">
|
||||
<UserAvatar />
|
||||
|
||||
<div className={styles.profileInputWrapper}>
|
||||
<label>{t['com.affine.settings.profile.name']()}</label>
|
||||
<FlexWrapper alignItems="center">
|
||||
<Input
|
||||
defaultValue={input}
|
||||
data-testid="user-name-input"
|
||||
placeholder={t['com.affine.settings.profile.placeholder']()}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
style={{ width: 280, height: 32 }}
|
||||
onChange={setInput}
|
||||
onEnter={handleUpdateUserName}
|
||||
/>
|
||||
{allowUpdate ? (
|
||||
<Button
|
||||
data-testid="save-user-name"
|
||||
onClick={handleUpdateUserName}
|
||||
style={{
|
||||
marginLeft: '12px',
|
||||
}}
|
||||
>
|
||||
{t['com.affine.editCollection.save']()}
|
||||
</Button>
|
||||
) : null}
|
||||
</FlexWrapper>
|
||||
</div>
|
||||
</FlexWrapper>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
const StoragePanel = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const onUpgrade = useCallback(() => {
|
||||
track.$.settingsPanel.accountUsage.viewPlans({
|
||||
plan: SubscriptionPlan.Pro,
|
||||
});
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.storage.title']()}
|
||||
desc=""
|
||||
spreadCol={false}
|
||||
>
|
||||
<StorageProgress onUpgrade={onUpgrade} />
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountSetting: FC = () => {
|
||||
const { authService, serverConfigService } = useServices({
|
||||
AuthService,
|
||||
ServerConfigService,
|
||||
});
|
||||
const serverFeatures = useLiveData(
|
||||
serverConfigService.serverConfig.features$
|
||||
);
|
||||
const t = useI18n();
|
||||
const session = authService.session;
|
||||
useEffect(() => {
|
||||
session.revalidate();
|
||||
}, [session]);
|
||||
const account = useEnsureLiveData(session.account$);
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
const setSignOutModal = useSetAtom(openSignOutModalAtom);
|
||||
|
||||
const onChangeEmail = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
// @ts-expect-error accont email is always defined
|
||||
email: account.email,
|
||||
emailType: account.info?.emailVerified ? 'changeEmail' : 'verifyEmail',
|
||||
});
|
||||
}, [account.email, account.info?.emailVerified, setAuthModal]);
|
||||
|
||||
const onPasswordButtonClick = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
// @ts-expect-error accont email is always defined
|
||||
email: account.email,
|
||||
emailType: account.info?.hasPassword ? 'changePassword' : 'setPassword',
|
||||
});
|
||||
}, [account.email, account.info?.hasPassword, setAuthModal]);
|
||||
|
||||
const onOpenSignOutModal = useCallback(() => {
|
||||
setSignOutModal(true);
|
||||
}, [setSignOutModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['com.affine.setting.account']()}
|
||||
subtitle={t['com.affine.setting.account.message']()}
|
||||
data-testid="account-title"
|
||||
/>
|
||||
<AvatarAndName />
|
||||
<SettingRow name={t['com.affine.settings.email']()} desc={account.email}>
|
||||
<Button onClick={onChangeEmail}>
|
||||
{account.info?.emailVerified
|
||||
? t['com.affine.settings.email.action.change']()
|
||||
: t['com.affine.settings.email.action.verify']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.password']()}
|
||||
desc={t['com.affine.settings.password.message']()}
|
||||
>
|
||||
<Button onClick={onPasswordButtonClick}>
|
||||
{account.info?.hasPassword
|
||||
? t['com.affine.settings.password.action.change']()
|
||||
: t['com.affine.settings.password.action.set']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<StoragePanel />
|
||||
{serverFeatures?.copilot && <AIUsagePanel />}
|
||||
<SettingRow
|
||||
name={t[`Sign out`]()}
|
||||
desc={t['com.affine.setting.sign.out.message']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
data-testid="sign-out-button"
|
||||
onClick={onOpenSignOutModal}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
{/*<SettingRow*/}
|
||||
{/* name={*/}
|
||||
{/* <span style={{ color: 'var(--affine-warning-color)' }}>*/}
|
||||
{/* {t['com.affine.setting.account.delete']()}*/}
|
||||
{/* </span>*/}
|
||||
{/* }*/}
|
||||
{/* desc={t['com.affine.setting.account.delete.message']()}*/}
|
||||
{/* style={{ cursor: 'pointer' }}*/}
|
||||
{/* onClick={useCallback(() => {*/}
|
||||
{/* toast('Function coming soon');*/}
|
||||
{/* }, [])}*/}
|
||||
{/* testId="delete-account-button"*/}
|
||||
{/*>*/}
|
||||
{/* <ArrowRightSmallIcon />*/}
|
||||
{/*</SettingRow>*/}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const storageProgressContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const storageProgressWrapper = style({
|
||||
flexGrow: 1,
|
||||
marginRight: '20px',
|
||||
});
|
||||
globalStyle(`${storageProgressWrapper} .storage-progress-desc`, {
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 2,
|
||||
});
|
||||
globalStyle(`${storageProgressWrapper} .storage-progress-bar-wrapper`, {
|
||||
height: '8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: cssVar('black10'),
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const storageProgressBar = style({
|
||||
height: '100%',
|
||||
});
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
import { Button, ErrorMessage, Skeleton, Tooltip } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
UserQuotaService,
|
||||
} from '../../../../modules/cloud';
|
||||
import * as styles from './storage-progress.css';
|
||||
|
||||
export interface StorageProgressProgress {
|
||||
upgradable?: boolean;
|
||||
onUpgrade: () => void;
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
Primary = 'primary',
|
||||
Default = 'secondary',
|
||||
}
|
||||
|
||||
export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => {
|
||||
const t = useI18n();
|
||||
const quota = useService(UserQuotaService).quota;
|
||||
|
||||
useEffect(() => {
|
||||
// revalidate quota to get the latest status
|
||||
quota.revalidate();
|
||||
}, [quota]);
|
||||
const color = useLiveData(quota.color$);
|
||||
const usedFormatted = useLiveData(quota.usedFormatted$);
|
||||
const maxFormatted = useLiveData(quota.maxFormatted$);
|
||||
const percent = useLiveData(quota.percent$);
|
||||
|
||||
const serverConfigService = useService(ServerConfigService);
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
||||
);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
useEffect(() => {
|
||||
// revalidate subscription to get the latest status
|
||||
subscription.revalidate();
|
||||
}, [subscription]);
|
||||
|
||||
const proSubscription = useLiveData(subscription.pro$);
|
||||
const isFreeUser = !proSubscription;
|
||||
const quotaName = useLiveData(
|
||||
quota.quota$.map(q => (q !== null ? q?.humanReadable.name : null))
|
||||
);
|
||||
|
||||
const loading =
|
||||
proSubscription === null || percent === null || quotaName === null;
|
||||
const loadError = useLiveData(quota.error$);
|
||||
|
||||
const buttonType = useMemo(() => {
|
||||
if (isFreeUser) {
|
||||
return ButtonType.Primary;
|
||||
}
|
||||
return ButtonType.Default;
|
||||
}, [isFreeUser]);
|
||||
|
||||
if (loading) {
|
||||
if (loadError) {
|
||||
// TODO(@catsjuice): i18n
|
||||
return <ErrorMessage>Load error</ErrorMessage>;
|
||||
}
|
||||
return <Skeleton height={42} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.storageProgressContainer}>
|
||||
<div className={styles.storageProgressWrapper}>
|
||||
<div className="storage-progress-desc">
|
||||
<span>{t['com.affine.storage.used.hint']()}</span>
|
||||
<span>
|
||||
{usedFormatted}/{maxFormatted}
|
||||
{` (${quotaName} ${t['com.affine.storage.plan']()})`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="storage-progress-bar-wrapper">
|
||||
<div
|
||||
className={styles.storageProgressBar}
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
backgroundColor: color ?? cssVar('processingColor'),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasPaymentFeature ? (
|
||||
<Tooltip
|
||||
options={{ hidden: percent < 100 }}
|
||||
content={
|
||||
isFreeUser
|
||||
? t['com.affine.storage.maximum-tips']()
|
||||
: t['com.affine.storage.maximum-tips.pro']()
|
||||
}
|
||||
>
|
||||
<span tabIndex={0}>
|
||||
<Button variant={buttonType} onClick={onUpgrade}>
|
||||
{isFreeUser
|
||||
? t['com.affine.storage.upgrade']()
|
||||
: t['com.affine.storage.change-plan']()}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const profileInputWrapper = style({
|
||||
marginLeft: '20px',
|
||||
});
|
||||
globalStyle(`${profileInputWrapper} label`, {
|
||||
display: 'block',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
marginBottom: '4px',
|
||||
});
|
||||
export const avatarWrapper = style({
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '50%',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
selectors: {
|
||||
'&.disable': {
|
||||
cursor: 'default',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||
display: 'flex',
|
||||
});
|
||||
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
display: 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(60, 61, 63, 0.5)',
|
||||
zIndex: '1',
|
||||
color: cssVar('white'),
|
||||
fontSize: cssVar('fontH4'),
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const settingModalScrollContainerAtom = atom<HTMLElement | null>(null);
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
RedditIcon,
|
||||
TelegramIcon,
|
||||
TwitterIcon,
|
||||
YouTubeIcon,
|
||||
} from './icons';
|
||||
|
||||
export const relatedLinks = [
|
||||
{
|
||||
icon: <GithubIcon />,
|
||||
title: 'GitHub',
|
||||
link: 'https://github.com/toeverything/AFFiNE',
|
||||
},
|
||||
{
|
||||
icon: <TwitterIcon />,
|
||||
title: 'X',
|
||||
link: 'https://twitter.com/AffineOfficial',
|
||||
},
|
||||
{
|
||||
icon: <DiscordIcon />,
|
||||
title: 'Discord',
|
||||
link: 'https://discord.gg/whd5mjYqVw',
|
||||
},
|
||||
{
|
||||
icon: <YouTubeIcon />,
|
||||
title: 'YouTube',
|
||||
link: 'https://www.youtube.com/@affinepro',
|
||||
},
|
||||
{
|
||||
icon: <TelegramIcon />,
|
||||
title: 'Telegram',
|
||||
link: 'https://t.me/affineworkos',
|
||||
},
|
||||
{
|
||||
icon: <RedditIcon />,
|
||||
title: 'Reddit',
|
||||
link: 'https://www.reddit.com/r/Affine/',
|
||||
},
|
||||
];
|
||||
-175
@@ -1,175 +0,0 @@
|
||||
// The icons here have been specially adjusted, they’re different from the ones in the @blocksuite/icons/rc.
|
||||
|
||||
export { TwitterIcon } from '@blocksuite/icons/rc';
|
||||
export const LogoIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.1996 0L4 50H14.0741L25.0146 15.4186L35.96 50H46L28.7978 0H21.1996Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const DocIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 40.5353V9.46462C2 6.95444 2.99716 4.54708 4.77212 2.77212C6.54708 0.997163 8.95444 0 11.4646 0H37.7552C39.0224 0 40.0497 1.02726 40.0497 2.29445V33.3652C40.0497 33.4357 40.0465 33.5055 40.0403 33.5744C39.9882 34.1502 39.7234 34.6646 39.3251 35.0385C38.9147 35.4237 38.3625 35.6597 37.7552 35.6597H11.4646C11.0129 35.6597 10.5676 35.7224 10.1404 35.8429C8.60419 36.2781 7.37011 37.4505 6.85245 38.9541C6.67955 39.4584 6.58891 39.9922 6.58891 40.5354C6.58891 41.8285 7.1026 43.0687 8.01697 43.983C8.93134 44.8974 10.1715 45.4111 11.4646 45.4111H42.6309V4.68456C42.6309 3.41736 43.6582 2.3901 44.9254 2.3901C46.1926 2.3901 47.2198 3.41736 47.2198 4.68456V47.7055C47.2198 48.9727 46.1926 50 44.9254 50H11.4646C8.95445 50 6.54708 49.0028 4.77212 47.2279C2.99716 45.4529 2 43.0456 2 40.5353ZM12.6596 38.2409C11.3925 38.2409 10.3652 39.2682 10.3652 40.5354C10.3652 41.8026 11.3925 42.8298 12.6596 42.8298H36.5602C37.8274 42.8298 38.8546 41.8026 38.8546 40.5354C38.8546 39.2682 37.8274 38.2409 36.5602 38.2409H12.6596Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GithubIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3073_4801)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.667 2C7.14199 2 2.66699 6.58819 2.66699 12.2529C2.66699 16.7899 5.52949 20.6219 9.50449 21.9804C10.0045 22.0701 10.192 21.7625 10.192 21.4934C10.192 21.2499 10.1795 20.4425 10.1795 19.5838C7.66699 20.058 7.01699 18.9558 6.81699 18.3791C6.70449 18.0843 6.21699 17.1743 5.79199 16.9308C5.44199 16.7386 4.94199 16.2644 5.77949 16.2516C6.56699 16.2388 7.12949 16.9949 7.31699 17.3025C8.21699 18.8533 9.65449 18.4175 10.2295 18.1484C10.317 17.4819 10.5795 17.0334 10.867 16.777C8.64199 16.5207 6.31699 15.6364 6.31699 11.7147C6.31699 10.5997 6.70449 9.67689 7.34199 8.95918C7.24199 8.70286 6.89199 7.65193 7.44199 6.24215C7.44199 6.24215 8.27949 5.97301 10.192 7.29308C10.992 7.06239 11.842 6.94704 12.692 6.94704C13.542 6.94704 14.392 7.06239 15.192 7.29308C17.1045 5.9602 17.942 6.24215 17.942 6.24215C18.492 7.65193 18.142 8.70286 18.042 8.95918C18.6795 9.67689 19.067 10.5868 19.067 11.7147C19.067 15.6492 16.7295 16.5207 14.5045 16.777C14.867 17.0975 15.1795 17.7126 15.1795 18.6738C15.1795 20.0452 15.167 21.1474 15.167 21.4934C15.167 21.7625 15.3545 22.0829 15.8545 21.9804C17.8396 21.2932 19.5646 19.9851 20.7867 18.2401C22.0088 16.4951 22.6664 14.4012 22.667 12.2529C22.667 6.58819 18.192 2 12.667 2Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3073_4801">
|
||||
<rect width="25" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const DiscordIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3073_4801)">
|
||||
<path
|
||||
d="M19.2565 5.64663C17.9898 5.05614 16.6183 4.62755 15.1897 4.37993C15.1772 4.37953 15.1647 4.38188 15.1532 4.38681C15.1417 4.39175 15.1314 4.39915 15.1231 4.4085C14.9516 4.72279 14.7516 5.13233 14.6183 5.44662C13.103 5.21804 11.562 5.21804 10.0467 5.44662C9.9134 5.1228 9.71339 4.72279 9.53243 4.4085C9.52291 4.38945 9.49434 4.37993 9.46576 4.37993C8.03715 4.62755 6.67521 5.05614 5.39899 5.64663C5.38946 5.64663 5.37994 5.65615 5.37041 5.66568C2.77987 9.54197 2.06556 13.3135 2.41795 17.0469C2.41795 17.066 2.42748 17.085 2.44652 17.0946C4.16086 18.3517 5.80852 19.1137 7.43714 19.6184C7.46571 19.628 7.49428 19.6184 7.50381 19.5994C7.88477 19.0756 8.22764 18.5232 8.52288 17.9422C8.54193 17.9041 8.52288 17.866 8.48479 17.8565C7.94191 17.647 7.42761 17.3993 6.92284 17.1136C6.88474 17.0946 6.88474 17.0374 6.91331 17.0088C7.01808 16.9327 7.12284 16.8469 7.22761 16.7707C7.24666 16.7517 7.27523 16.7517 7.29428 16.7612C10.5706 18.2565 14.104 18.2565 17.3422 16.7612C17.3612 16.7517 17.3898 16.7517 17.4088 16.7707C17.5136 16.8565 17.6184 16.9327 17.7231 17.0184C17.7612 17.0469 17.7612 17.1041 17.7136 17.1231C17.2184 17.4184 16.6945 17.6565 16.1517 17.866C16.1136 17.8755 16.104 17.9232 16.1136 17.9517C16.4183 18.5327 16.7612 19.0851 17.1326 19.6089C17.1612 19.6184 17.1898 19.628 17.2184 19.6184C18.8565 19.1137 20.5042 18.3517 22.2185 17.0946C22.2375 17.085 22.2471 17.066 22.2471 17.0469C22.6661 12.7325 21.5518 8.98958 19.2946 5.66568C19.2851 5.65615 19.2756 5.64663 19.2565 5.64663ZM9.01813 14.7707C8.03715 14.7707 7.21808 13.8659 7.21808 12.7516C7.21808 11.6373 8.01811 10.7325 9.01813 10.7325C10.0277 10.7325 10.8277 11.6468 10.8182 12.7516C10.8182 13.8659 10.0182 14.7707 9.01813 14.7707ZM15.6564 14.7707C14.6754 14.7707 13.8564 13.8659 13.8564 12.7516C13.8564 11.6373 14.6564 10.7325 15.6564 10.7325C16.666 10.7325 17.466 11.6468 17.4565 12.7516C17.4565 13.8659 16.666 14.7707 15.6564 14.7707Z"
|
||||
fill="#5865F2"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3073_4801">
|
||||
<rect width="25" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TelegramIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2C9.34844 2 6.80312 3.05422 4.92969 4.92891C3.05432 6.80434 2.00052 9.34778 2 12C2 14.6511 3.05469 17.1964 4.92969 19.0711C6.80312 20.9458 9.34844 22 12 22C14.6516 22 17.1969 20.9458 19.0703 19.0711C20.9453 17.1964 22 14.6511 22 12C22 9.34891 20.9453 6.80359 19.0703 4.92891C17.1969 3.05422 14.6516 2 12 2Z"
|
||||
fill="url(#paint0_linear_8233_169329)"
|
||||
/>
|
||||
<path
|
||||
d="M6.5267 11.8943C9.44232 10.6243 11.3861 9.78694 12.3579 9.38241C15.1361 8.22726 15.7126 8.02663 16.0892 8.01983C16.172 8.01851 16.3564 8.03898 16.4767 8.13624C16.5767 8.21827 16.6048 8.32921 16.6189 8.4071C16.6314 8.48491 16.6486 8.66226 16.6345 8.80069C16.4845 10.3819 15.8329 14.2191 15.5017 15.9902C15.3626 16.7396 15.0861 16.9908 14.8189 17.0154C14.2376 17.0688 13.797 16.6316 13.2345 16.263C12.3548 15.686 11.8579 15.3269 11.0033 14.764C10.0158 14.1134 10.6564 13.7557 11.2189 13.1713C11.3658 13.0184 13.9251 10.691 13.9736 10.4799C13.9798 10.4535 13.9861 10.3551 13.9267 10.3032C13.8689 10.2512 13.7829 10.269 13.7204 10.283C13.6314 10.303 12.2267 11.2324 9.5017 13.071C9.10326 13.3451 8.74232 13.4787 8.41732 13.4716C8.06107 13.464 7.37357 13.2698 6.86264 13.1038C6.23764 12.9002 5.7392 12.7926 5.78295 12.4468C5.80482 12.2668 6.05326 12.0826 6.5267 11.8943Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_8233_169329"
|
||||
x1="1002"
|
||||
y1="2"
|
||||
x2="1002"
|
||||
y2="2002"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2AABEE" />
|
||||
<stop offset="1" stopColor="#229ED9" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const RedditIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.334 22C17.8568 22 22.334 17.5228 22.334 12C22.334 6.47715 17.8568 2 12.334 2C6.81114 2 2.33398 6.47715 2.33398 12C2.33398 17.5228 6.81114 22 12.334 22Z"
|
||||
fill="#FF4500"
|
||||
/>
|
||||
<path
|
||||
d="M18.9863 12.0954C18.9863 11.2848 18.3308 10.641 17.5319 10.641C17.1545 10.6404 16.7915 10.7857 16.5186 11.0463C15.5172 10.331 14.1461 9.86611 12.6202 9.8065L13.2877 6.68299L15.4574 7.14783C15.4814 7.69627 15.9343 8.13744 16.4948 8.13744C17.067 8.13744 17.5319 7.6726 17.5319 7.1001C17.5319 6.52791 17.067 6.06299 16.4948 6.06299C16.0895 6.06299 15.7316 6.30143 15.5648 6.64721L13.1448 6.13455C13.0732 6.12252 13.0016 6.13455 12.9539 6.17033C12.8943 6.20611 12.8586 6.26564 12.8468 6.33721L12.1074 9.8183C10.5577 9.86611 9.16273 10.331 8.14945 11.0583C7.87653 10.7976 7.51349 10.6524 7.13609 10.653C6.32539 10.653 5.68164 11.3085 5.68164 12.1074C5.68164 12.7035 6.03922 13.2041 6.54008 13.4308C6.51576 13.5766 6.50379 13.7241 6.5043 13.8719C6.5043 16.113 9.11524 17.9372 12.3341 17.9372C15.553 17.9372 18.1639 16.125 18.1639 13.8719C18.1638 13.7241 18.1519 13.5766 18.1281 13.4308C18.6288 13.2041 18.9863 12.6914 18.9863 12.0954ZM8.99586 13.1325C8.99586 12.5603 9.4607 12.0954 10.0332 12.0954C10.6054 12.0954 11.0703 12.5603 11.0703 13.1325C11.0703 13.7048 10.6055 14.1699 10.0332 14.1699C9.46078 14.1816 8.99586 13.7048 8.99586 13.1325ZM14.8019 15.8865C14.0866 16.6019 12.7274 16.6496 12.3341 16.6496C11.9288 16.6496 10.5697 16.5899 9.86609 15.8865C9.75898 15.7792 9.75898 15.6123 9.86609 15.505C9.97344 15.3979 10.1403 15.3979 10.2477 15.505C10.7008 15.9581 11.6545 16.113 12.3341 16.113C13.0137 16.113 13.9792 15.9581 14.4203 15.505C14.5277 15.3979 14.6945 15.3979 14.8019 15.505C14.8972 15.6123 14.8972 15.7792 14.8019 15.8865ZM14.611 14.1817C14.0387 14.1817 13.5739 13.7168 13.5739 13.1446C13.5739 12.5723 14.0387 12.1074 14.611 12.1074C15.1834 12.1074 15.6483 12.5723 15.6483 13.1446C15.6483 13.7047 15.1834 14.1817 14.611 14.1817Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const LinkIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.2917 1.33334C10.2917 0.988166 10.5715 0.708344 10.9167 0.708344H14.6667C15.0118 0.708344 15.2917 0.988166 15.2917 1.33334V5.08334C15.2917 5.42852 15.0118 5.70834 14.6667 5.70834C14.3215 5.70834 14.0417 5.42852 14.0417 5.08334V2.84223L8.44194 8.44195C8.19787 8.68603 7.80214 8.68603 7.55806 8.44195C7.31398 8.19787 7.31398 7.80215 7.55806 7.55807L13.1578 1.95834H10.9167C10.5715 1.95834 10.2917 1.67852 10.2917 1.33334ZM3.97464 1.54168L7.58334 1.54168C7.92851 1.54168 8.20834 1.8215 8.20834 2.16668C8.20834 2.51185 7.92851 2.79168 7.58334 2.79168H4C3.52298 2.79168 3.2028 2.79216 2.95623 2.81231C2.71697 2.83186 2.60256 2.86676 2.5271 2.90521C2.33109 3.00508 2.17174 3.16443 2.07187 3.36044C2.03342 3.4359 1.99852 3.55031 1.97897 3.78957C1.95882 4.03614 1.95834 4.35632 1.95834 4.83334V12C1.95834 12.477 1.95882 12.7972 1.97897 13.0438C1.99852 13.283 2.03342 13.3974 2.07187 13.4729C2.17174 13.6689 2.33109 13.8283 2.5271 13.9281C2.60256 13.9666 2.71697 14.0015 2.95623 14.021C3.2028 14.0412 3.52298 14.0417 4 14.0417H11.1667C11.6437 14.0417 11.9639 14.0412 12.2104 14.021C12.4497 14.0015 12.5641 13.9666 12.6396 13.9281C12.8356 13.8283 12.9949 13.6689 13.0948 13.4729C13.1333 13.3974 13.1682 13.283 13.1877 13.0438C13.2079 12.7972 13.2083 12.477 13.2083 12V8.41668C13.2083 8.0715 13.4882 7.79168 13.8333 7.79168C14.1785 7.79168 14.4583 8.0715 14.4583 8.41668V12.0254C14.4583 12.4705 14.4584 12.842 14.4336 13.1456C14.4077 13.4621 14.3518 13.7594 14.2086 14.0404C13.9888 14.4716 13.6383 14.8222 13.2071 15.0419C12.926 15.1851 12.6288 15.241 12.3122 15.2669C12.0087 15.2917 11.6372 15.2917 11.192 15.2917H3.97463C3.5295 15.2917 3.15797 15.2917 2.85444 15.2669C2.53787 15.241 2.24066 15.1851 1.95961 15.0419C1.5284 14.8222 1.17782 14.4716 0.958113 14.0404C0.81491 13.7594 0.758984 13.4621 0.733119 13.1456C0.70832 12.842 0.708327 12.4705 0.708336 12.0254V4.80798C0.708327 4.36285 0.70832 3.99131 0.733119 3.68779C0.758984 3.37121 0.81491 3.074 0.958113 2.79295C1.17782 2.36174 1.5284 2.01116 1.95961 1.79145C2.24066 1.64825 2.53787 1.59232 2.85444 1.56646C3.15797 1.54166 3.52951 1.54167 3.97464 1.54168Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const YouTubeIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.7477 7.19232C21.6387 6.76858 21.4261 6.38227 21.1311 6.07186C20.8361 5.76145 20.4689 5.53776 20.0662 5.42308C18.5917 5 12.6575 5 12.6575 5C12.6575 5 6.72304 5.01281 5.24858 5.43589C4.84583 5.55057 4.47865 5.77427 4.18363 6.0847C3.88861 6.39512 3.67602 6.78145 3.56705 7.2052C3.12106 9.96155 2.94806 14.1616 3.5793 16.8077C3.68828 17.2314 3.90087 17.6177 4.19589 17.9281C4.49092 18.2386 4.85808 18.4622 5.26083 18.5769C6.73528 19 12.6696 19 12.6696 19C12.6696 19 18.6039 19 20.0783 18.5769C20.481 18.4623 20.8482 18.2386 21.1432 17.9282C21.4383 17.6177 21.6509 17.2314 21.7599 16.8077C22.2303 14.0474 22.3752 9.85004 21.7477 7.1924V7.19232Z"
|
||||
fill="#FF0000"
|
||||
/>
|
||||
<path d="M10.667 15L15.667 12L10.667 9V15Z" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
import { Switch } from '@affine/component';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { useAppUpdater } from '@affine/core/components/hooks/use-app-updater';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import { appIconMap, appNames } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { mixpanel } from '@affine/track';
|
||||
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
|
||||
import { relatedLinks } from './config';
|
||||
import * as styles from './style.css';
|
||||
import { UpdateCheckSection } from './update-check-section';
|
||||
|
||||
export const AboutAffine = () => {
|
||||
const t = useI18n();
|
||||
const { urlService, featureFlagService } = useServices({
|
||||
UrlService,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||
const { toggleAutoCheck, toggleAutoDownload } = useAppUpdater();
|
||||
const enableSnapshotImportExport = useLiveData(
|
||||
featureFlagService.flags.enable_snapshot_import_export.$
|
||||
);
|
||||
const channel = BUILD_CONFIG.appBuildType;
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
|
||||
const onSwitchAutoCheck = useCallback(
|
||||
(checked: boolean) => {
|
||||
toggleAutoCheck(checked);
|
||||
updateSettings('autoCheckUpdate', checked);
|
||||
},
|
||||
[toggleAutoCheck, updateSettings]
|
||||
);
|
||||
|
||||
const onSwitchAutoDownload = useCallback(
|
||||
(checked: boolean) => {
|
||||
toggleAutoDownload(checked);
|
||||
updateSettings('autoDownloadUpdate', checked);
|
||||
},
|
||||
[toggleAutoDownload, updateSettings]
|
||||
);
|
||||
|
||||
const onSwitchTelemetry = useCallback(
|
||||
(checked: boolean) => {
|
||||
if (!checked) {
|
||||
mixpanel.opt_out_tracking();
|
||||
} else {
|
||||
mixpanel.opt_in_tracking();
|
||||
}
|
||||
updateSettings('enableTelemetry', checked);
|
||||
},
|
||||
[updateSettings]
|
||||
);
|
||||
|
||||
const onSwitchSnapshotImportExport = useCallback(
|
||||
(checked: boolean) => {
|
||||
featureFlagService.flags.enable_snapshot_import_export.set(checked);
|
||||
},
|
||||
[featureFlagService]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['com.affine.aboutAFFiNE.title']()}
|
||||
subtitle={t['com.affine.aboutAFFiNE.subtitle']()}
|
||||
data-testid="about-title"
|
||||
/>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.version.title']()}>
|
||||
<SettingRow
|
||||
name={appName}
|
||||
desc={BUILD_CONFIG.appVersion}
|
||||
className={styles.appImageRow}
|
||||
>
|
||||
<img src={appIcon} alt={appName} width={56} height={56} />
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.version.editor.title']()}
|
||||
desc={BUILD_CONFIG.editorVersion}
|
||||
/>
|
||||
{BUILD_CONFIG.isElectron ? (
|
||||
<>
|
||||
<UpdateCheckSection />
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.autoCheckUpdate.title']()}
|
||||
desc={t['com.affine.aboutAFFiNE.autoCheckUpdate.description']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={onSwitchAutoCheck}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.autoDownloadUpdate.title']()}
|
||||
desc={t[
|
||||
'com.affine.aboutAFFiNE.autoDownloadUpdate.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoDownloadUpdate}
|
||||
onChange={onSwitchAutoDownload}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.changelog.title']()}
|
||||
desc={t['com.affine.aboutAFFiNE.changelog.description']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
urlService.openPopupWindow(BUILD_CONFIG.changelogUrl);
|
||||
}}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
</>
|
||||
) : null}
|
||||
<SettingRow
|
||||
name={t['com.affine.telemetry.enable']()}
|
||||
desc={t['com.affine.telemetry.enable.desc']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.enableTelemetry !== false}
|
||||
onChange={onSwitchTelemetry}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.contact.title']()}>
|
||||
<a
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
>
|
||||
{t['com.affine.aboutAFFiNE.contact.website']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<a
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
href="https://community.affine.pro"
|
||||
target="_blank"
|
||||
>
|
||||
{t['com.affine.aboutAFFiNE.contact.community']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<SettingRow
|
||||
name={t['com.affine.snapshot.import-export.enable']()}
|
||||
desc={t['com.affine.snapshot.import-export.enable.desc']()}
|
||||
className={styles.snapshotImportExportRow}
|
||||
>
|
||||
<Switch
|
||||
checked={enableSnapshotImportExport}
|
||||
onChange={onSwitchSnapshotImportExport}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.community.title']()}>
|
||||
<div className={styles.communityWrapper}>
|
||||
{relatedLinks.map(({ icon, title, link }) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.communityItem}
|
||||
onClick={() => {
|
||||
urlService.openPopupWindow(link);
|
||||
}}
|
||||
key={title}
|
||||
>
|
||||
{icon}
|
||||
<p>{title}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.legal.title']()}>
|
||||
<a
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
href="https://affine.pro/privacy"
|
||||
target="_blank"
|
||||
>
|
||||
{t['com.affine.aboutAFFiNE.legal.privacy']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<a
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
href="https://affine.pro/terms"
|
||||
target="_blank"
|
||||
>
|
||||
{t['com.affine.aboutAFFiNE.legal.tos']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
</SettingWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const link = style({
|
||||
height: '18px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 600,
|
||||
marginBottom: '12px',
|
||||
selectors: {
|
||||
'&:last-of-type': {
|
||||
marginBottom: '0',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${link} .icon`, {
|
||||
color: cssVar('iconColor'),
|
||||
fontSize: cssVar('fontBase'),
|
||||
marginLeft: '5px',
|
||||
});
|
||||
export const communityWrapper = style({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '15% 15% 15% 15% 15% 15%',
|
||||
gap: '2%',
|
||||
});
|
||||
export const communityItem = style({
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
color: cssVar('textPrimaryColor'),
|
||||
cursor: 'pointer',
|
||||
padding: '6px 8px',
|
||||
});
|
||||
globalStyle(`${communityItem} svg`, {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'block',
|
||||
margin: '0 auto 2px',
|
||||
});
|
||||
globalStyle(`${communityItem} p`, {
|
||||
fontSize: cssVar('fontXs'),
|
||||
textAlign: 'center',
|
||||
});
|
||||
export const checkUpdateDesc = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
selectors: {
|
||||
'&.active': {
|
||||
color: cssVar('textEmphasisColor'),
|
||||
},
|
||||
'&.error': {
|
||||
color: cssVar('errorColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${checkUpdateDesc} svg`, {
|
||||
marginRight: '4px',
|
||||
});
|
||||
export const appImageRow = style({
|
||||
flexDirection: 'row-reverse',
|
||||
selectors: {
|
||||
'&.two-col': {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${appImageRow} .right-col`, {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '20px',
|
||||
});
|
||||
export const snapshotImportExportRow = style({
|
||||
marginTop: '12px',
|
||||
});
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
import { Loading } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useAppUpdater } from '@affine/core/components/hooks/use-app-updater';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
enum CheckUpdateStatus {
|
||||
UNCHECK = 'uncheck',
|
||||
LATEST = 'latest',
|
||||
UPDATE_AVAILABLE = 'update-available',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
const useUpdateStatusLabels = (checkUpdateStatus: CheckUpdateStatus) => {
|
||||
const t = useI18n();
|
||||
const { updateAvailable, downloadProgress, updateReady, checkingForUpdates } =
|
||||
useAppUpdater();
|
||||
|
||||
const buttonLabel = useMemo(() => {
|
||||
if (updateReady) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.button.restart']();
|
||||
}
|
||||
if (updateAvailable && downloadProgress === null) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.button.download']();
|
||||
}
|
||||
if (
|
||||
checkUpdateStatus === CheckUpdateStatus.LATEST ||
|
||||
checkUpdateStatus === CheckUpdateStatus.ERROR
|
||||
) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.button.retry']();
|
||||
}
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.button.check']();
|
||||
}, [checkUpdateStatus, downloadProgress, t, updateAvailable, updateReady]);
|
||||
|
||||
const subtitleLabel = useMemo(() => {
|
||||
if (updateReady) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.restart']();
|
||||
} else if (updateAvailable && downloadProgress === null) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.update-available']({
|
||||
version: updateAvailable.version,
|
||||
});
|
||||
} else if (checkingForUpdates) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.checking']();
|
||||
} else if (updateAvailable && downloadProgress !== null) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.downloading']();
|
||||
} else if (checkUpdateStatus === CheckUpdateStatus.ERROR) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.error']();
|
||||
} else if (checkUpdateStatus === CheckUpdateStatus.LATEST) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.latest']();
|
||||
}
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.check']();
|
||||
}, [
|
||||
checkUpdateStatus,
|
||||
downloadProgress,
|
||||
checkingForUpdates,
|
||||
t,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
]);
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
return (
|
||||
<span
|
||||
className={clsx(styles.checkUpdateDesc, {
|
||||
active:
|
||||
updateReady ||
|
||||
(updateAvailable && downloadProgress === null) ||
|
||||
checkUpdateStatus === CheckUpdateStatus.LATEST,
|
||||
error: checkUpdateStatus === CheckUpdateStatus.ERROR,
|
||||
})}
|
||||
>
|
||||
{checkingForUpdates ? <Loading size={14} /> : null}
|
||||
{subtitleLabel}
|
||||
</span>
|
||||
);
|
||||
}, [
|
||||
checkUpdateStatus,
|
||||
downloadProgress,
|
||||
checkingForUpdates,
|
||||
subtitleLabel,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
]);
|
||||
|
||||
return { subtitle, buttonLabel };
|
||||
};
|
||||
|
||||
export const UpdateCheckSection = () => {
|
||||
const t = useI18n();
|
||||
const {
|
||||
checkForUpdates,
|
||||
downloadUpdate,
|
||||
quitAndInstall,
|
||||
updateAvailable,
|
||||
downloadProgress,
|
||||
updateReady,
|
||||
} = useAppUpdater();
|
||||
const [checkUpdateStatus, setCheckUpdateStatus] = useState<CheckUpdateStatus>(
|
||||
CheckUpdateStatus.UNCHECK
|
||||
);
|
||||
|
||||
const { buttonLabel, subtitle } = useUpdateStatusLabels(checkUpdateStatus);
|
||||
|
||||
const asyncCheckForUpdates = useAsyncCallback(async () => {
|
||||
let statusCheck = CheckUpdateStatus.UNCHECK;
|
||||
try {
|
||||
const status = await checkForUpdates();
|
||||
|
||||
if (status === null) {
|
||||
statusCheck = CheckUpdateStatus.ERROR;
|
||||
} else if (status === false) {
|
||||
statusCheck = CheckUpdateStatus.LATEST;
|
||||
} else if (typeof status === 'string') {
|
||||
statusCheck = CheckUpdateStatus.UPDATE_AVAILABLE;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
statusCheck = CheckUpdateStatus.ERROR;
|
||||
} finally {
|
||||
setCheckUpdateStatus(statusCheck);
|
||||
}
|
||||
}, [checkForUpdates]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (updateAvailable && downloadProgress === null) {
|
||||
return downloadUpdate();
|
||||
}
|
||||
if (updateReady) {
|
||||
return quitAndInstall();
|
||||
}
|
||||
asyncCheckForUpdates();
|
||||
}, [
|
||||
asyncCheckForUpdates,
|
||||
downloadProgress,
|
||||
downloadUpdate,
|
||||
quitAndInstall,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.checkUpdate.title']()}
|
||||
desc={subtitle}
|
||||
>
|
||||
<Button
|
||||
data-testid="check-update-button"
|
||||
onClick={handleClick}
|
||||
disabled={downloadProgress !== null && !updateReady}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
import type { RadioItem } from '@affine/component';
|
||||
import { RadioGroup, Switch } from '@affine/component';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
|
||||
import { LanguageMenu } from '../../../language-menu';
|
||||
import { OpenInAppLinksMenu } from './links';
|
||||
import { settingWrapper } from './style.css';
|
||||
import { ThemeEditorSetting } from './theme-editor-setting';
|
||||
|
||||
export const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
|
||||
[
|
||||
{
|
||||
value: 'system',
|
||||
label: t['com.affine.themeSettings.system'](),
|
||||
testId: 'system-theme-trigger',
|
||||
},
|
||||
{
|
||||
value: 'light',
|
||||
label: t['com.affine.themeSettings.light'](),
|
||||
testId: 'light-theme-trigger',
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: t['com.affine.themeSettings.dark'](),
|
||||
testId: 'dark-theme-trigger',
|
||||
},
|
||||
] satisfies RadioItem[];
|
||||
|
||||
export const ThemeSettings = () => {
|
||||
const t = useI18n();
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
const radioItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
items={radioItems}
|
||||
value={theme}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={useCallback(
|
||||
(value: string) => {
|
||||
setTheme(value);
|
||||
},
|
||||
[setTheme]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppearanceSettings = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableThemeEditor = useLiveData(
|
||||
featureFlagService.flags.enable_theme_editor.$
|
||||
);
|
||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['com.affine.appearanceSettings.title']()}
|
||||
subtitle={t['com.affine.appearanceSettings.subtitle']()}
|
||||
/>
|
||||
|
||||
<SettingWrapper title={t['com.affine.appearanceSettings.theme.title']()}>
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.color.title']()}
|
||||
desc={t['com.affine.appearanceSettings.color.description']()}
|
||||
>
|
||||
<ThemeSettings />
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.language.title']()}
|
||||
desc={t['com.affine.appearanceSettings.language.description']()}
|
||||
>
|
||||
<div className={settingWrapper}>
|
||||
<LanguageMenu />
|
||||
</div>
|
||||
</SettingRow>
|
||||
{BUILD_CONFIG.isElectron ? (
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.clientBorder.title']()}
|
||||
desc={t['com.affine.appearanceSettings.clientBorder.description']()}
|
||||
data-testid="client-border-style-trigger"
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.clientBorder}
|
||||
onChange={checked => updateSettings('clientBorder', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
) : null}
|
||||
{enableThemeEditor ? <ThemeEditorSetting /> : null}
|
||||
</SettingWrapper>
|
||||
|
||||
{BUILD_CONFIG.isWeb && !environment.isMobile ? (
|
||||
<SettingWrapper title={t['com.affine.setting.appearance.links']()}>
|
||||
<SettingRow
|
||||
name={t['com.affine.setting.appearance.open-in-app']()}
|
||||
desc={t['com.affine.setting.appearance.open-in-app.hint']()}
|
||||
data-testid="open-in-app-links-trigger"
|
||||
>
|
||||
<OpenInAppLinksMenu />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
) : null}
|
||||
{BUILD_CONFIG.isElectron ? (
|
||||
<SettingWrapper
|
||||
title={t['com.affine.appearanceSettings.sidebar.title']()}
|
||||
>
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.noisyBackground.title']()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.noisyBackground.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.enableNoisyBackground}
|
||||
onChange={checked =>
|
||||
updateSettings('enableNoisyBackground', checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
{environment.isMacOs && (
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.translucentUI.title']()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.translucentUI.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.enableBlurBackground}
|
||||
onChange={checked =>
|
||||
updateSettings('enableBlurBackground', checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
)}
|
||||
</SettingWrapper>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const menu = style({
|
||||
background: cssVar('white'),
|
||||
width: '250px',
|
||||
maxHeight: '30vh',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
|
||||
export const menuItem = style({
|
||||
color: cssVar('textPrimaryColor'),
|
||||
selectors: {
|
||||
'&[data-selected=true]': {
|
||||
color: cssVar('primaryColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
|
||||
import {
|
||||
OpenInAppService,
|
||||
OpenLinkMode,
|
||||
} from '@affine/core/modules/open-in-app';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './links.css';
|
||||
|
||||
export const OpenInAppLinksMenu = () => {
|
||||
const t = useI18n();
|
||||
const openInAppService = useService(OpenInAppService);
|
||||
const currentOpenInAppMode = useLiveData(openInAppService.openLinkMode$);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
Object.values(OpenLinkMode).map(mode => ({
|
||||
label:
|
||||
t.t(`com.affine.setting.appearance.open-in-app.${mode}`) ||
|
||||
`com.affine.setting.appearance.open-in-app.${mode}`,
|
||||
value: mode,
|
||||
})),
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
items={options.map(option => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
title={option.label}
|
||||
onSelect={() => openInAppService.setOpenLinkMode(option.value)}
|
||||
data-selected={currentOpenInAppMode === option.value}
|
||||
>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
contentOptions={{
|
||||
className: styles.menu,
|
||||
align: 'end',
|
||||
}}
|
||||
>
|
||||
<MenuTrigger style={{ fontWeight: 600, width: '250px' }} block={true}>
|
||||
{options.find(option => option.value === currentOpenInAppMode)?.label}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const settingWrapper = style({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
minWidth: '150px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api/service';
|
||||
import { ThemeEditorService } from '@affine/core/modules/theme-editor';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import { DeleteIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
} from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const ThemeEditorSetting = () => {
|
||||
const themeEditor = useService(ThemeEditorService);
|
||||
const modified = useLiveData(themeEditor.modified$);
|
||||
const urlService = useService(UrlService);
|
||||
const desktopApi = useServiceOptional(DesktopApiService);
|
||||
|
||||
const open = useCallback(() => {
|
||||
if (desktopApi) {
|
||||
desktopApi.handler.ui.openThemeEditor().catch(console.error);
|
||||
} else if (BUILD_CONFIG.isMobileWeb || BUILD_CONFIG.isWeb) {
|
||||
urlService.openPopupWindow(location.origin + '/theme-editor');
|
||||
}
|
||||
}, [desktopApi, urlService]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name="Customize Theme"
|
||||
desc="Edit all AFFiNE theme variables here"
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
{modified ? (
|
||||
<Button
|
||||
style={{
|
||||
color: cssVar('errorColor'),
|
||||
borderColor: cssVar('errorColor'),
|
||||
}}
|
||||
prefixStyle={{
|
||||
color: cssVar('errorColor'),
|
||||
}}
|
||||
onClick={() => themeEditor.reset()}
|
||||
variant="secondary"
|
||||
prefix={<DeleteIcon />}
|
||||
>
|
||||
Reset all
|
||||
</Button>
|
||||
) : null}
|
||||
<Button onClick={open}>Open Theme Editor</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
-636
@@ -1,636 +0,0 @@
|
||||
import { Skeleton } from '@affine/component';
|
||||
import { Pagination } from '@affine/component/member-components';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { Button, IconButton } from '@affine/component/ui/button';
|
||||
import { Loading } from '@affine/component/ui/loading';
|
||||
import { getUpgradeQuestionnaireLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import {
|
||||
AuthService,
|
||||
InvoicesService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import type { InvoicesQuery } from '@affine/graphql';
|
||||
import {
|
||||
createCustomerPortalMutation,
|
||||
InvoiceStatus,
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
SubscriptionStatus,
|
||||
UserFriendlyError,
|
||||
} from '@affine/graphql';
|
||||
import { type I18nString, i18nTime, Trans, useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useMutation } from '../../../../../components/hooks/use-mutation';
|
||||
import {
|
||||
openSettingModalAtom,
|
||||
type PlansScrollAnchor,
|
||||
} from '../../../../atoms';
|
||||
import { CancelAction, ResumeAction } from '../plans/actions';
|
||||
import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions';
|
||||
import { AIRedeemCodeButton } from '../plans/ai/actions/redeem';
|
||||
import { BelieverCard } from '../plans/lifetime/believer-card';
|
||||
import { BelieverBenefits } from '../plans/lifetime/benefits';
|
||||
import { RedeemCode } from '../plans/plan-card';
|
||||
import * as styles from './style.css';
|
||||
|
||||
const DescriptionI18NKey = {
|
||||
Basic: 'com.affine.payment.billing-setting.current-plan.description',
|
||||
Monthly:
|
||||
'com.affine.payment.billing-setting.current-plan.description.monthly',
|
||||
Yearly: 'com.affine.payment.billing-setting.current-plan.description.yearly',
|
||||
Lifetime:
|
||||
'com.affine.payment.billing-setting.current-plan.description.lifetime',
|
||||
} as const satisfies { [key: string]: I18nString };
|
||||
|
||||
const getMessageKey = (
|
||||
plan: SubscriptionPlan,
|
||||
recurring: SubscriptionRecurring
|
||||
) => {
|
||||
if (plan !== SubscriptionPlan.Pro) {
|
||||
return DescriptionI18NKey.Basic;
|
||||
}
|
||||
return DescriptionI18NKey[recurring];
|
||||
};
|
||||
|
||||
export const BillingSettings = () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['com.affine.payment.billing-setting.title']()}
|
||||
subtitle={t['com.affine.payment.billing-setting.subtitle']()}
|
||||
/>
|
||||
<SettingWrapper
|
||||
title={t['com.affine.payment.billing-setting.information']()}
|
||||
>
|
||||
<SubscriptionSettings />
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.payment.billing-setting.history']()}>
|
||||
<BillingHistory />
|
||||
</SettingWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SubscriptionSettings = () => {
|
||||
const t = useI18n();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
useEffect(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
const proSubscription = useLiveData(subscriptionService.subscription.pro$);
|
||||
const proPrice = useLiveData(subscriptionService.prices.proPrice$);
|
||||
const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$);
|
||||
const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$);
|
||||
|
||||
const [openCancelModal, setOpenCancelModal] = useState(false);
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const currentPlan = proSubscription?.plan ?? SubscriptionPlan.Free;
|
||||
const currentRecurring =
|
||||
proSubscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
|
||||
const openPlans = useCallback(
|
||||
(scrollAnchor?: PlansScrollAnchor) => {
|
||||
track.$.settingsPanel.billing.viewPlans();
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: scrollAnchor,
|
||||
});
|
||||
},
|
||||
[setOpenSettingModalAtom]
|
||||
);
|
||||
const gotoCloudPlansSetting = useCallback(
|
||||
() => openPlans('cloudPricingPlan'),
|
||||
[openPlans]
|
||||
);
|
||||
const gotoAiPlanSetting = useCallback(
|
||||
() => openPlans('aiPricingPlan'),
|
||||
[openPlans]
|
||||
);
|
||||
|
||||
const amount = proSubscription
|
||||
? proPrice
|
||||
? proSubscription.recurring === SubscriptionRecurring.Monthly
|
||||
? String((proPrice.amount ?? 0) / 100)
|
||||
: String((proPrice.yearlyAmount ?? 0) / 100)
|
||||
: '?'
|
||||
: '0';
|
||||
|
||||
return (
|
||||
<div className={styles.subscription}>
|
||||
<AIPlanCard onClick={gotoAiPlanSetting} />
|
||||
{/* loaded */}
|
||||
{proSubscription !== null ? (
|
||||
isBeliever ? (
|
||||
<BelieverIdentifier onOpenPlans={gotoCloudPlansSetting} />
|
||||
) : (
|
||||
<div className={styles.planCard}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name={t['com.affine.payment.billing-setting.current-plan']()}
|
||||
desc={
|
||||
<>
|
||||
<Trans
|
||||
i18nKey={getMessageKey(currentPlan, currentRecurring)}
|
||||
values={{
|
||||
planName: currentPlan,
|
||||
}}
|
||||
components={{
|
||||
1: (
|
||||
<span
|
||||
onClick={gotoCloudPlansSetting}
|
||||
className={styles.currentPlanName}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<CloudExpirationInfo />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<PlanAction
|
||||
plan={currentPlan}
|
||||
gotoPlansSetting={gotoCloudPlansSetting}
|
||||
/>
|
||||
</div>
|
||||
<p className={styles.planPrice}>
|
||||
${amount}
|
||||
<span className={styles.billingFrequency}>
|
||||
/
|
||||
{currentRecurring === SubscriptionRecurring.Monthly
|
||||
? t['com.affine.payment.billing-setting.month']()
|
||||
: t['com.affine.payment.billing-setting.year']()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<SubscriptionSettingSkeleton />
|
||||
)}
|
||||
|
||||
<TypeFormLink />
|
||||
|
||||
{proSubscription !== null ? (
|
||||
proSubscription?.status === SubscriptionStatus.Active && (
|
||||
<>
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name={t['com.affine.payment.billing-setting.payment-method']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.payment-method.description'
|
||||
]()}
|
||||
>
|
||||
<PaymentMethodUpdater />
|
||||
</SettingRow>
|
||||
{isBeliever || isOnetime ? null : proSubscription.end &&
|
||||
proSubscription.canceledAt ? (
|
||||
<SettingRow
|
||||
name={t['com.affine.payment.billing-setting.expiration-date']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.expiration-date.description'
|
||||
]({
|
||||
expirationDate: new Date(
|
||||
proSubscription.end
|
||||
).toLocaleDateString(),
|
||||
})}
|
||||
>
|
||||
<ResumeSubscription />
|
||||
</SettingRow>
|
||||
) : (
|
||||
<CancelAction
|
||||
open={openCancelModal}
|
||||
onOpenChange={setOpenCancelModal}
|
||||
>
|
||||
<SettingRow
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setOpenCancelModal(true);
|
||||
}}
|
||||
className="dangerous-setting"
|
||||
name={t[
|
||||
'com.affine.payment.billing-setting.cancel-subscription'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.cancel-subscription.description'
|
||||
]()}
|
||||
>
|
||||
<CancelSubscription />
|
||||
</SettingRow>
|
||||
</CancelAction>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<SubscriptionSettingSkeleton />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CloudExpirationInfo = () => {
|
||||
const t = useI18n();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const subscription = useLiveData(subscriptionService.subscription.pro$);
|
||||
|
||||
let text = '';
|
||||
if (subscription?.nextBillAt) {
|
||||
text = t['com.affine.payment.billing-setting.renew-date.description']({
|
||||
renewDate: i18nTime(subscription.nextBillAt, {
|
||||
absolute: { accuracy: 'day' },
|
||||
}),
|
||||
});
|
||||
} else if (subscription?.end) {
|
||||
text = t['com.affine.payment.billing-setting.due-date.description']({
|
||||
dueDate: i18nTime(subscription.end, {
|
||||
absolute: { accuracy: 'day' },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return text ? (
|
||||
<>
|
||||
<br />
|
||||
{text}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const TypeFormLink = () => {
|
||||
const t = useI18n();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const pro = useLiveData(subscriptionService.subscription.pro$);
|
||||
const ai = useLiveData(subscriptionService.subscription.ai$);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
|
||||
if (!account) return null;
|
||||
if (!pro && !ai) return null;
|
||||
|
||||
const plan = [];
|
||||
if (pro) plan.push(SubscriptionPlan.Pro);
|
||||
if (ai) plan.push(SubscriptionPlan.AI);
|
||||
|
||||
const link = getUpgradeQuestionnaireLink({
|
||||
name: account.info?.name,
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
recurring: pro?.recurring ?? ai?.recurring ?? SubscriptionRecurring.Yearly,
|
||||
plan,
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name={t['com.affine.payment.billing-type-form.title']()}
|
||||
desc={t['com.affine.payment.billing-type-form.description']()}
|
||||
>
|
||||
<a target="_blank" href={link} rel="noreferrer">
|
||||
<Button>{t['com.affine.payment.billing-type-form.go']()}</Button>
|
||||
</a>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
const BelieverIdentifier = ({ onOpenPlans }: { onOpenPlans?: () => void }) => {
|
||||
const t = useI18n();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const readableLifetimePrice = useLiveData(
|
||||
subscriptionService.prices.readableLifetimePrice$
|
||||
);
|
||||
|
||||
if (!readableLifetimePrice) return null;
|
||||
|
||||
return (
|
||||
<BelieverCard type={2} style={{ borderRadius: 8, padding: 12 }}>
|
||||
<header className={styles.believerHeader}>
|
||||
<div>
|
||||
<div className={styles.believerTitle}>
|
||||
{t['com.affine.payment.billing-setting.believer.title']()}
|
||||
</div>
|
||||
<div className={styles.believerSubtitle}>
|
||||
<Trans
|
||||
i18nKey={
|
||||
'com.affine.payment.billing-setting.believer.description'
|
||||
}
|
||||
components={{
|
||||
a: <a href="#" onClick={onOpenPlans} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.believerPriceWrapper}>
|
||||
<div className={styles.believerPrice}>{readableLifetimePrice}</div>
|
||||
<div className={styles.believerPriceCaption}>
|
||||
{t['com.affine.payment.billing-setting.believer.price-caption']()}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<BelieverBenefits />
|
||||
</BelieverCard>
|
||||
);
|
||||
};
|
||||
|
||||
const AIPlanCard = ({ onClick }: { onClick: () => void }) => {
|
||||
const t = useI18n();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
useEffect(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||
const subscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$);
|
||||
|
||||
const priceReadable = price?.yearlyAmount
|
||||
? `$${(price.yearlyAmount / 100).toFixed(2)}`
|
||||
: '?';
|
||||
const priceFrequency = t['com.affine.payment.billing-setting.year']();
|
||||
|
||||
if (subscription === null) {
|
||||
return <Skeleton height={100} />;
|
||||
}
|
||||
|
||||
const billingTip =
|
||||
subscription === undefined ? (
|
||||
<Trans
|
||||
i18nKey={'com.affine.payment.billing-setting.ai.free-desc'}
|
||||
components={{
|
||||
a: (
|
||||
<a href="#" onClick={onClick} className={styles.currentPlanName} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : subscription?.nextBillAt ? (
|
||||
t['com.affine.payment.ai.billing-tip.next-bill-at']({
|
||||
due: i18nTime(subscription.nextBillAt, {
|
||||
absolute: { accuracy: 'day' },
|
||||
}),
|
||||
})
|
||||
) : (isOnetime || subscription?.canceledAt) && subscription.end ? (
|
||||
t['com.affine.payment.ai.billing-tip.end-at']({
|
||||
end: i18nTime(subscription.end, { absolute: { accuracy: 'day' } }),
|
||||
})
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={styles.planCard} style={{ marginBottom: 24 }}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name={t['com.affine.payment.billing-setting.ai-plan']()}
|
||||
desc={billingTip}
|
||||
/>
|
||||
{price?.yearlyAmount ? (
|
||||
subscription ? (
|
||||
isOnetime ? (
|
||||
<AIRedeemCodeButton className={styles.planAction} />
|
||||
) : subscription.canceledAt ? (
|
||||
<AIResume className={styles.planAction} />
|
||||
) : (
|
||||
<AICancel className={styles.planAction} />
|
||||
)
|
||||
) : (
|
||||
<AISubscribe className={styles.planAction}>
|
||||
{t['com.affine.payment.billing-setting.ai.purchase']()}
|
||||
</AISubscribe>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<p className={styles.planPrice}>
|
||||
{subscription ? priceReadable : '$0'}
|
||||
<span className={styles.billingFrequency}>/{priceFrequency}</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PlanAction = ({
|
||||
plan,
|
||||
gotoPlansSetting,
|
||||
}: {
|
||||
plan: string;
|
||||
gotoPlansSetting: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
const isOnetimePro = useLiveData(subscription.isOnetimePro$);
|
||||
|
||||
if (isOnetimePro) {
|
||||
return <RedeemCode variant="primary" className={styles.planAction} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
variant="primary"
|
||||
onClick={gotoPlansSetting}
|
||||
>
|
||||
{plan === SubscriptionPlan.Pro
|
||||
? t['com.affine.payment.billing-setting.change-plan']()
|
||||
: t['com.affine.payment.billing-setting.upgrade']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const PaymentMethodUpdater = () => {
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: createCustomerPortalMutation,
|
||||
});
|
||||
const urlService = useService(UrlService);
|
||||
const t = useI18n();
|
||||
|
||||
const update = useAsyncCallback(async () => {
|
||||
await trigger(null, {
|
||||
onSuccess: data => {
|
||||
urlService.openPopupWindow(data.createCustomerPortal);
|
||||
},
|
||||
});
|
||||
}, [trigger, urlService]);
|
||||
|
||||
return (
|
||||
<Button onClick={update} loading={isMutating} disabled={isMutating}>
|
||||
{t['com.affine.payment.billing-setting.update']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const ResumeSubscription = () => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResumeAction open={open} onOpenChange={setOpen}>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
data-event-props="$.settingsPanel.plans.resumeSubscription"
|
||||
data-event-args-type={subscription.pro$.value?.plan}
|
||||
data-event-args-category={subscription.pro$.value?.recurring}
|
||||
>
|
||||
{t['com.affine.payment.billing-setting.resume-subscription']()}
|
||||
</Button>
|
||||
</ResumeAction>
|
||||
);
|
||||
};
|
||||
|
||||
const CancelSubscription = ({ loading }: { loading?: boolean }) => {
|
||||
return (
|
||||
<IconButton
|
||||
style={{ pointerEvents: 'none' }}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
const BillingHistory = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const invoicesService = useService(InvoicesService);
|
||||
const pageInvoices = useLiveData(invoicesService.invoices.pageInvoices$);
|
||||
const invoiceCount = useLiveData(invoicesService.invoices.invoiceCount$);
|
||||
const isLoading = useLiveData(invoicesService.invoices.isLoading$);
|
||||
const error = useLiveData(invoicesService.invoices.error$);
|
||||
const pageNum = useLiveData(invoicesService.invoices.pageNum$);
|
||||
|
||||
useEffect(() => {
|
||||
invoicesService.invoices.revalidate();
|
||||
}, [invoicesService]);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(_: number, pageNum: number) => {
|
||||
invoicesService.invoices.setPageNum(pageNum);
|
||||
invoicesService.invoices.revalidate();
|
||||
},
|
||||
[invoicesService]
|
||||
);
|
||||
|
||||
if (invoiceCount === undefined) {
|
||||
if (isLoading) {
|
||||
return <BillingHistorySkeleton />;
|
||||
} else {
|
||||
return (
|
||||
<span style={{ color: cssVar('errorColor') }}>
|
||||
{error
|
||||
? UserFriendlyError.fromAnyError(error).message
|
||||
: 'Failed to load members'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.history}>
|
||||
<div className={styles.historyContent}>
|
||||
{invoiceCount === 0 ? (
|
||||
<p className={styles.noInvoice}>
|
||||
{t['com.affine.payment.billing-setting.no-invoice']()}
|
||||
</p>
|
||||
) : (
|
||||
pageInvoices?.map(invoice => (
|
||||
<InvoiceLine key={invoice.id} invoice={invoice} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{invoiceCount > invoicesService.invoices.PAGE_SIZE && (
|
||||
<Pagination
|
||||
totalCount={invoiceCount}
|
||||
countPerPage={invoicesService.invoices.PAGE_SIZE}
|
||||
pageNum={pageNum}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InvoiceLine = ({
|
||||
invoice,
|
||||
}: {
|
||||
invoice: NonNullable<InvoicesQuery['currentUser']>['invoices'][0];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const urlService = useService(UrlService);
|
||||
|
||||
const open = useCallback(() => {
|
||||
if (invoice.link) {
|
||||
urlService.openPopupWindow(invoice.link);
|
||||
}
|
||||
}, [invoice.link, urlService]);
|
||||
|
||||
const planText =
|
||||
invoice.plan === SubscriptionPlan.AI
|
||||
? 'AFFiNE AI'
|
||||
: invoice.plan === SubscriptionPlan.Pro
|
||||
? invoice.recurring === SubscriptionRecurring.Lifetime
|
||||
? 'AFFiNE Cloud Believer'
|
||||
: 'AFFiNE Cloud'
|
||||
: null;
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
key={invoice.id}
|
||||
name={new Date(invoice.createdAt).toLocaleDateString()}
|
||||
desc={`${
|
||||
invoice.status === InvoiceStatus.Paid
|
||||
? t['com.affine.payment.billing-setting.paid']()
|
||||
: ''
|
||||
} $${invoice.amount / 100} - ${planText}`}
|
||||
>
|
||||
<Button onClick={open}>
|
||||
{t['com.affine.payment.billing-setting.view-invoice']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
const SubscriptionSettingSkeleton = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<SettingWrapper
|
||||
title={t['com.affine.payment.billing-setting.information']()}
|
||||
>
|
||||
<div className={styles.subscriptionSettingSkeleton}>
|
||||
<Skeleton variant="rounded" height="104px" />
|
||||
<Skeleton variant="rounded" height="46px" />
|
||||
</div>
|
||||
</SettingWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const BillingHistorySkeleton = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<SettingWrapper title={t['com.affine.payment.billing-setting.history']()}>
|
||||
<div className={styles.billingHistorySkeleton}>
|
||||
<Loading />
|
||||
</div>
|
||||
</SettingWrapper>
|
||||
);
|
||||
};
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const subscription = style({});
|
||||
export const history = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '24px',
|
||||
});
|
||||
export const historyContent = style({
|
||||
width: '100%',
|
||||
});
|
||||
export const planCard = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px',
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
borderRadius: '8px',
|
||||
});
|
||||
export const currentPlan = style({
|
||||
flex: '1 0 0',
|
||||
});
|
||||
export const planAction = style({
|
||||
width: 'auto',
|
||||
marginTop: '8px',
|
||||
});
|
||||
export const planPrice = style({
|
||||
fontSize: cssVar('fontH6'),
|
||||
fontWeight: 600,
|
||||
});
|
||||
export const billingFrequency = style({
|
||||
fontSize: cssVar('fontBase'),
|
||||
});
|
||||
export const paymentMethod = style({
|
||||
marginTop: '24px',
|
||||
});
|
||||
globalStyle('.dangerous-setting .name', {
|
||||
color: cssVar('errorColor'),
|
||||
});
|
||||
export const noInvoice = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
export const currentPlanName = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 500,
|
||||
color: cssVar('textEmphasisColor'),
|
||||
cursor: 'pointer',
|
||||
});
|
||||
export const subscriptionSettingSkeleton = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '24px',
|
||||
});
|
||||
export const billingHistorySkeleton = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '72px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
// believer-identification
|
||||
export const believerHeader = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
});
|
||||
export const believerTitle = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
});
|
||||
export const believerSubtitle = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
fontWeight: 400,
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
globalStyle(`.${believerSubtitle} > a`, {
|
||||
color: cssVar('brandColor'),
|
||||
fontWeight: 500,
|
||||
});
|
||||
export const believerPriceWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'end',
|
||||
});
|
||||
export const believerPrice = style({
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
lineHeight: '26px',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
});
|
||||
export const believerPriceCaption = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
fontWeight: 500,
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
-566
@@ -1,566 +0,0 @@
|
||||
import {
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
RadioGroup,
|
||||
type RadioItem,
|
||||
Slider,
|
||||
} from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
ConnectorMode,
|
||||
FontFamily,
|
||||
FontFamilyMap,
|
||||
FontStyle,
|
||||
FontWeightMap,
|
||||
LineColor,
|
||||
LineColorMap,
|
||||
PointStyle,
|
||||
StrokeStyle,
|
||||
TextAlign,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { useFramework, useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { DropdownMenu } from '../menu';
|
||||
import { menuTrigger, settingWrapper } from '../style.css';
|
||||
import { sortedFontWeightEntries, useColor } from '../utils';
|
||||
import { Point } from './point';
|
||||
import { EdgelessSnapshot } from './snapshot';
|
||||
import { getSurfaceBlock } from './utils';
|
||||
|
||||
enum ConnecterStyle {
|
||||
General = 'general',
|
||||
Scribbled = 'scribbled',
|
||||
}
|
||||
|
||||
enum ConnectorTextFontSize {
|
||||
'16px' = '16',
|
||||
'20px' = '20',
|
||||
'24px' = '24',
|
||||
'32px' = '32',
|
||||
'40px' = '40',
|
||||
'64px' = '64',
|
||||
}
|
||||
|
||||
export const ConnectorSettings = () => {
|
||||
const t = useI18n();
|
||||
const framework = useFramework();
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
const settings = useLiveData(editorSetting.settings$);
|
||||
const getColorFromMap = useColor();
|
||||
|
||||
const connecterStyleItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: ConnecterStyle.General,
|
||||
label: t['com.affine.settings.editorSettings.edgeless.style.general'](),
|
||||
},
|
||||
{
|
||||
value: ConnecterStyle.Scribbled,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.style.scribbled'](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
const connecterStyle: ConnecterStyle = settings.connector.rough
|
||||
? ConnecterStyle.Scribbled
|
||||
: ConnecterStyle.General;
|
||||
const setConnecterStyle = useCallback(
|
||||
(value: ConnecterStyle) => {
|
||||
const isRough = value === ConnecterStyle.Scribbled;
|
||||
editorSetting.set('connector', {
|
||||
rough: isRough,
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const connectorShapeItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: ConnectorMode.Orthogonal as any,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.elbowed'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: ConnectorMode.Curve as any,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.curve'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: ConnectorMode.Straight as any,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.straight'
|
||||
](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
const connectorShape: ConnectorMode = settings.connector.mode;
|
||||
const setConnectorShape = useCallback(
|
||||
(value: ConnectorMode) => {
|
||||
editorSetting.set('connector', {
|
||||
mode: value,
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const borderStyleItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: StrokeStyle.Solid,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
|
||||
},
|
||||
{
|
||||
value: StrokeStyle.Dash,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
const borderStyle: StrokeStyle = settings.connector.strokeStyle;
|
||||
const setBorderStyle = useCallback(
|
||||
(value: StrokeStyle) => {
|
||||
editorSetting.set('connector', {
|
||||
strokeStyle: value,
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const borderThickness = settings.connector.strokeWidth;
|
||||
const setBorderThickness = useCallback(
|
||||
(value: number[]) => {
|
||||
editorSetting.set('connector', {
|
||||
strokeWidth: value[0],
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const currentColor = useMemo(() => {
|
||||
const color = settings.connector.stroke;
|
||||
return getColorFromMap(color, LineColorMap);
|
||||
}, [getColorFromMap, settings.connector.stroke]);
|
||||
|
||||
const colorItems = useMemo(() => {
|
||||
const { stroke } = settings.connector;
|
||||
return Object.entries(LineColor).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', { stroke: value });
|
||||
};
|
||||
const isSelected = stroke === value;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
onSelect={handler}
|
||||
selected={isSelected}
|
||||
prefix={<Point color={value} />}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const startEndPointItems = useMemo(() => {
|
||||
const { frontEndpointStyle } = settings.connector;
|
||||
return Object.entries(PointStyle).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', { frontEndpointStyle: value });
|
||||
};
|
||||
const isSelected = frontEndpointStyle === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const endEndPointItems = useMemo(() => {
|
||||
const { rearEndpointStyle } = settings.connector;
|
||||
return Object.entries(PointStyle).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', { rearEndpointStyle: value });
|
||||
};
|
||||
const isSelected = rearEndpointStyle === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const alignItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: TextAlign.Left,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.left'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: TextAlign.Center,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.center'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: TextAlign.Right,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.right'
|
||||
](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const textAlignment = settings.connector.labelStyle.textAlign;
|
||||
const setTextAlignment = useCallback(
|
||||
(value: TextAlign) => {
|
||||
editorSetting.set('connector', {
|
||||
labelStyle: {
|
||||
textAlign: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const fontFamilyItems = useMemo(() => {
|
||||
const { fontFamily } = settings.connector.labelStyle;
|
||||
return Object.entries(FontFamily).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', {
|
||||
labelStyle: {
|
||||
fontFamily: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
const isSelected = fontFamily === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const fontStyleItems = useMemo(() => {
|
||||
const { fontStyle } = settings.connector.labelStyle;
|
||||
return Object.entries(FontStyle).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', {
|
||||
labelStyle: {
|
||||
fontStyle: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
const isSelected = fontStyle === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const fontWeightItems = useMemo(() => {
|
||||
const { fontWeight } = settings.connector.labelStyle;
|
||||
return sortedFontWeightEntries.map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', {
|
||||
labelStyle: {
|
||||
fontWeight: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
const isSelected = fontWeight === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const fontSizeItems = useMemo(() => {
|
||||
const { fontSize } = settings.connector.labelStyle;
|
||||
return Object.entries(ConnectorTextFontSize).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', {
|
||||
labelStyle: {
|
||||
fontSize: Number(value),
|
||||
},
|
||||
});
|
||||
};
|
||||
const isSelected = fontSize === Number(value);
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const textColorItems = useMemo(() => {
|
||||
const { color } = settings.connector.labelStyle;
|
||||
return Object.entries(LineColor).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', {
|
||||
labelStyle: {
|
||||
color: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
const isSelected = color === value;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
onSelect={handler}
|
||||
selected={isSelected}
|
||||
prefix={<Point color={value} />}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const textColor = useMemo(() => {
|
||||
const { color } = settings.connector.labelStyle;
|
||||
return getColorFromMap(color, LineColorMap);
|
||||
}, [getColorFromMap, settings]);
|
||||
|
||||
const getElements = useCallback((doc: Doc) => {
|
||||
const surface = getSurfaceBlock(doc);
|
||||
return surface?.getElementsByType('connector') || [];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EdgelessSnapshot
|
||||
title={t['com.affine.settings.editorSettings.edgeless.connecter']()}
|
||||
docName="connector"
|
||||
keyName="connector"
|
||||
getElements={getElements}
|
||||
/>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.color'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
{currentColor ? (
|
||||
<DropdownMenu
|
||||
items={colorItems}
|
||||
trigger={
|
||||
<MenuTrigger
|
||||
className={menuTrigger}
|
||||
prefix={<Point color={currentColor.value} />}
|
||||
>
|
||||
{currentColor.key}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.style']()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={connecterStyleItems}
|
||||
value={connecterStyle}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setConnecterStyle}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={connectorShapeItems}
|
||||
value={connectorShape}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setConnectorShape}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.border-style'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={borderStyleItems}
|
||||
value={borderStyle}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setBorderStyle}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.border-thickness'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<Slider
|
||||
value={[borderThickness]}
|
||||
onValueChange={setBorderThickness}
|
||||
min={2}
|
||||
max={12}
|
||||
step={2}
|
||||
nodes={[2, 4, 6, 8, 10, 12]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.start-endpoint'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={startEndPointItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{String(settings.connector.frontEndpointStyle)}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.connecter.end-endpoint'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={endEndPointItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{String(settings.connector.rearEndpointStyle)}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.text-color'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
{textColor ? (
|
||||
<DropdownMenu
|
||||
items={textColorItems}
|
||||
trigger={
|
||||
<MenuTrigger
|
||||
className={menuTrigger}
|
||||
prefix={<Point color={textColor.value} />}
|
||||
>
|
||||
{textColor.key}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-family'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontFamilyItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{FontFamilyMap[settings.connector.labelStyle.fontFamily]}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.font-size'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontSizeItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{settings.connector.labelStyle.fontSize + 'px'}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-style'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontStyleItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{settings.connector.labelStyle.fontStyle}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-weight'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontWeightItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{FontWeightMap[settings.connector.labelStyle.fontWeight]}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.text-alignment'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={alignItems}
|
||||
value={textAlignment}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setTextAlignment}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"type": "page",
|
||||
"meta": {
|
||||
"id": "gJEFfmo4QJ",
|
||||
"title": "",
|
||||
"createDate": 1726038448921,
|
||||
"tags": []
|
||||
},
|
||||
"blocks": {
|
||||
"type": "block",
|
||||
"id": "orzKfiHevj",
|
||||
"flavour": "affine:page",
|
||||
"version": 2,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": []
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "-48EmppaxI",
|
||||
"flavour": "affine:surface",
|
||||
"version": 5,
|
||||
"props": {
|
||||
"elements": {
|
||||
"m_PwyyI76y": {
|
||||
"index": "a0",
|
||||
"seed": 44330892,
|
||||
"frontEndpointStyle": "None",
|
||||
"labelOffset": {
|
||||
"distance": 0.5,
|
||||
"anchor": "center"
|
||||
},
|
||||
"labelStyle": {
|
||||
"color": "--affine-palette-line-black",
|
||||
"fontSize": 16,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontWeight": "400",
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "center"
|
||||
},
|
||||
"labelXYWH": [235.6484375, 65.23828125, 200, 20],
|
||||
"mode": 2,
|
||||
"rearEndpointStyle": "Arrow",
|
||||
"rough": false,
|
||||
"roughness": 1.4,
|
||||
"source": {
|
||||
"position": [120.8515625, 146.44921875]
|
||||
},
|
||||
"stroke": "--affine-palette-line-grey",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"target": {
|
||||
"position": [387.4453125, 4.02734375]
|
||||
},
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "label"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "connector",
|
||||
"id": "m_PwyyI76y"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "-6UhNH7qhy",
|
||||
"flavour": "affine:frame",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Frame 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": "--affine-palette-transparent",
|
||||
"xywh": "[-12.04296875,-49.66796875,542.9765625,248.3984375]",
|
||||
"index": "Zz",
|
||||
"childElementIds": {
|
||||
"m_PwyyI76y": true
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
-470
@@ -1,470 +0,0 @@
|
||||
{
|
||||
"type": "page",
|
||||
"meta": {
|
||||
"id": "IWDwzafoLh",
|
||||
"title": "BlockSuite Playground",
|
||||
"createDate": 1725541560333,
|
||||
"tags": []
|
||||
},
|
||||
"blocks": {
|
||||
"type": "block",
|
||||
"id": "DBwzSu70O4",
|
||||
"flavour": "affine:page",
|
||||
"version": 2,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "BlockSuite Playground"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "VsLifn6v6J",
|
||||
"flavour": "affine:surface",
|
||||
"version": 5,
|
||||
"props": {
|
||||
"elements": {
|
||||
"03ESqmbu0V": {
|
||||
"index": "aF",
|
||||
"seed": 456881005,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0.1,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Start"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 1,
|
||||
"xywh": "[252.60701568907035,-31.240866187007754,113.98566757331668,46.66666793823242]",
|
||||
"type": "shape",
|
||||
"id": "03ESqmbu0V"
|
||||
},
|
||||
"YQDvi-2y7O": {
|
||||
"index": "aG",
|
||||
"seed": 1257533881,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Action"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 1,
|
||||
"xywh": "[251.34653068910762,63.28169306573089,116.50663757324219,46.66666793823242]",
|
||||
"type": "shape",
|
||||
"id": "YQDvi-2y7O"
|
||||
},
|
||||
"d16Jq63Ef-": {
|
||||
"index": "aH",
|
||||
"seed": 1257533881,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Action"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 1,
|
||||
"xywh": "[480.71602985081984,63.28169306573089,116.50663757324219,46.66666793823242]",
|
||||
"type": "shape",
|
||||
"id": "d16Jq63Ef-"
|
||||
},
|
||||
"pFUd4r8HSb": {
|
||||
"index": "aI",
|
||||
"seed": 666256980,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "diamond",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Yes/No"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 1,
|
||||
"xywh": "[259.6798856390588,158.60141743948986,99.83992767333984,100]",
|
||||
"type": "shape",
|
||||
"id": "pFUd4r8HSb"
|
||||
},
|
||||
"WC0DtV40eR": {
|
||||
"index": "aJ",
|
||||
"seed": 1257533881,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Action"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 1,
|
||||
"xywh": "[251.3465118415371,306.9211418132488,116.50663757324219,47]",
|
||||
"type": "shape",
|
||||
"id": "WC0DtV40eR"
|
||||
},
|
||||
"L8IRBW8JgC": {
|
||||
"index": "aK",
|
||||
"seed": 456881005,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0.1,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "End"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 1,
|
||||
"xywh": "[251.21161577561134,402.2408661870077,116.77642970509373,47]",
|
||||
"type": "shape",
|
||||
"id": "L8IRBW8JgC"
|
||||
},
|
||||
"jnVx3KzCLh": {
|
||||
"index": "aR",
|
||||
"seed": 914130882,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 16,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "triangle",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textResizing": 1,
|
||||
"xywh": "[16.110706599375533,93.96971522611781,99.95313668402997,80.60737422459296]",
|
||||
"type": "shape",
|
||||
"id": "jnVx3KzCLh"
|
||||
},
|
||||
"hXY08VN1b_": {
|
||||
"index": "aS",
|
||||
"seed": 1037406953,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 16,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "ellipse",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textResizing": 1,
|
||||
"xywh": "[16.110706599375547,226.48127342070092,97.3493072661882,97.90333942510793]",
|
||||
"type": "shape",
|
||||
"id": "hXY08VN1b_"
|
||||
},
|
||||
"-aHPKrKHll": {
|
||||
"index": "aL",
|
||||
"seed": 1201328314,
|
||||
"frontEndpointStyle": "None",
|
||||
"mode": 1,
|
||||
"rearEndpointStyle": "Arrow",
|
||||
"rough": false,
|
||||
"roughness": 1.4,
|
||||
"source": {
|
||||
"id": "03ESqmbu0V",
|
||||
"position": [0.5, 1]
|
||||
},
|
||||
"stroke": "--affine-palette-line-grey",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"target": {
|
||||
"id": "YQDvi-2y7O",
|
||||
"position": [0.5, 0]
|
||||
},
|
||||
"type": "connector",
|
||||
"id": "-aHPKrKHll"
|
||||
},
|
||||
"9nFcoL2dRX": {
|
||||
"index": "aM",
|
||||
"seed": 1062067961,
|
||||
"frontEndpointStyle": "None",
|
||||
"mode": 1,
|
||||
"rearEndpointStyle": "Arrow",
|
||||
"rough": false,
|
||||
"roughness": 1.4,
|
||||
"source": {
|
||||
"id": "YQDvi-2y7O",
|
||||
"position": [1, 0.5]
|
||||
},
|
||||
"stroke": "--affine-palette-line-grey",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"target": {
|
||||
"id": "d16Jq63Ef-",
|
||||
"position": [0, 0.49997133993897247]
|
||||
},
|
||||
"type": "connector",
|
||||
"id": "9nFcoL2dRX"
|
||||
},
|
||||
"j4pebF9wC1": {
|
||||
"index": "aN",
|
||||
"seed": 970063880,
|
||||
"frontEndpointStyle": "Arrow",
|
||||
"mode": 1,
|
||||
"rearEndpointStyle": "None",
|
||||
"rough": false,
|
||||
"roughness": 1.4,
|
||||
"source": {
|
||||
"id": "d16Jq63Ef-",
|
||||
"position": [0.5, 1]
|
||||
},
|
||||
"stroke": "--affine-palette-line-grey",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"target": {
|
||||
"id": "pFUd4r8HSb",
|
||||
"position": [1, 0.5]
|
||||
},
|
||||
"type": "connector",
|
||||
"id": "j4pebF9wC1"
|
||||
},
|
||||
"WUp1wz3qjl": {
|
||||
"index": "aO",
|
||||
"seed": 628182641,
|
||||
"frontEndpointStyle": "None",
|
||||
"mode": 1,
|
||||
"rearEndpointStyle": "Arrow",
|
||||
"rough": false,
|
||||
"roughness": 1.4,
|
||||
"source": {
|
||||
"id": "YQDvi-2y7O",
|
||||
"position": [0.5, 1]
|
||||
},
|
||||
"stroke": "--affine-palette-line-grey",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"target": {
|
||||
"id": "pFUd4r8HSb",
|
||||
"position": [0.5, 0]
|
||||
},
|
||||
"type": "connector",
|
||||
"id": "WUp1wz3qjl"
|
||||
},
|
||||
"LW-DOsZkzx": {
|
||||
"index": "aP",
|
||||
"seed": 1308365768,
|
||||
"frontEndpointStyle": "None",
|
||||
"mode": 1,
|
||||
"rearEndpointStyle": "Arrow",
|
||||
"rough": false,
|
||||
"roughness": 1.4,
|
||||
"source": {
|
||||
"id": "pFUd4r8HSb",
|
||||
"position": [0.5, 1]
|
||||
},
|
||||
"stroke": "--affine-palette-line-grey",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"target": {
|
||||
"id": "WC0DtV40eR",
|
||||
"position": [0.5, 0]
|
||||
},
|
||||
"type": "connector",
|
||||
"id": "LW-DOsZkzx"
|
||||
},
|
||||
"iFv5piS4UF": {
|
||||
"index": "aQ",
|
||||
"seed": 782147341,
|
||||
"frontEndpointStyle": "None",
|
||||
"mode": 1,
|
||||
"rearEndpointStyle": "Arrow",
|
||||
"rough": false,
|
||||
"roughness": 1.4,
|
||||
"source": {
|
||||
"id": "WC0DtV40eR",
|
||||
"position": [0.5, 1]
|
||||
},
|
||||
"stroke": "--affine-palette-line-grey",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"target": {
|
||||
"id": "L8IRBW8JgC",
|
||||
"position": [0.5, 0]
|
||||
},
|
||||
"type": "connector",
|
||||
"id": "iFv5piS4UF"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "LHrmmlDo4Q",
|
||||
"flavour": "affine:edgeless-text",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"xywh": "[30.80080405395168,20.987504771361614,71.2421875,56]",
|
||||
"index": "aE",
|
||||
"color": "--affine-palette-line-black",
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontStyle": "normal",
|
||||
"fontWeight": "400",
|
||||
"textAlign": "left",
|
||||
"scale": 1,
|
||||
"rotate": 0,
|
||||
"hasMaxWidth": false
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "qc2TtDkPRq",
|
||||
"flavour": "affine:paragraph",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"type": "text",
|
||||
"text": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Others"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "cmuL9JhfVH",
|
||||
"flavour": "affine:frame",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Frame 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": "--affine-palette-transparent",
|
||||
"xywh": "[-131.27862548828125,-112.8125,880.65234375,645.11328125]",
|
||||
"index": "aEV",
|
||||
"childElementIds": {}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import { AffineSchemas } from '@blocksuite/affine/blocks';
|
||||
import type { Doc, DocSnapshot } from '@blocksuite/affine/store';
|
||||
import { DocCollection, Job, Schema } from '@blocksuite/affine/store';
|
||||
|
||||
const getCollection = (() => {
|
||||
let collection: DocCollection | null = null;
|
||||
return async function () {
|
||||
if (collection) {
|
||||
return collection;
|
||||
}
|
||||
const schema = new Schema();
|
||||
schema.register(AffineSchemas);
|
||||
collection = new DocCollection({ schema });
|
||||
collection.meta.initialize();
|
||||
return collection;
|
||||
};
|
||||
})();
|
||||
|
||||
export type DocName =
|
||||
| 'note'
|
||||
| 'pen'
|
||||
| 'shape'
|
||||
| 'flow'
|
||||
| 'text'
|
||||
| 'connector'
|
||||
| 'mindmap';
|
||||
|
||||
const docMap = new Map<DocName, Promise<Doc | undefined>>();
|
||||
|
||||
async function loadNote() {
|
||||
return (await import('./note.json')).default;
|
||||
}
|
||||
|
||||
async function loadPen() {
|
||||
return (await import('./pen.json')).default;
|
||||
}
|
||||
|
||||
async function loadShape() {
|
||||
return (await import('./shape.json')).default;
|
||||
}
|
||||
|
||||
async function loadFlow() {
|
||||
return (await import('./flow.json')).default;
|
||||
}
|
||||
|
||||
async function loadText() {
|
||||
return (await import('./text.json')).default;
|
||||
}
|
||||
|
||||
async function loadConnector() {
|
||||
return (await import('./connector.json')).default;
|
||||
}
|
||||
|
||||
async function loadMindmap() {
|
||||
return (await import('./mindmap.json')).default;
|
||||
}
|
||||
|
||||
const loaders = {
|
||||
note: loadNote,
|
||||
pen: loadPen,
|
||||
shape: loadShape,
|
||||
flow: loadFlow,
|
||||
text: loadText,
|
||||
connector: loadConnector,
|
||||
mindmap: loadMindmap,
|
||||
};
|
||||
|
||||
export async function getDocByName(name: DocName) {
|
||||
if (docMap.get(name)) {
|
||||
return docMap.get(name);
|
||||
}
|
||||
|
||||
const promise = initDoc(name);
|
||||
docMap.set(name, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function initDoc(name: DocName) {
|
||||
const snapshot = (await loaders[name]()) as DocSnapshot;
|
||||
const collection = await getCollection();
|
||||
const job = new Job({
|
||||
collection,
|
||||
middlewares: [],
|
||||
});
|
||||
|
||||
return await job.snapshotToDoc(snapshot);
|
||||
}
|
||||
-242
@@ -1,242 +0,0 @@
|
||||
{
|
||||
"type": "page",
|
||||
"meta": {
|
||||
"id": "_JUmoI_F28",
|
||||
"title": "BlockSuite Playground",
|
||||
"createDate": 1725610677620,
|
||||
"tags": []
|
||||
},
|
||||
"blocks": {
|
||||
"type": "block",
|
||||
"id": "FnaWN8Zm2_",
|
||||
"flavour": "affine:page",
|
||||
"version": 2,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "BlockSuite Playground"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "hevWf4ccWc",
|
||||
"flavour": "affine:surface",
|
||||
"version": 5,
|
||||
"props": {
|
||||
"elements": {
|
||||
"MYyOCcLTWN": {
|
||||
"index": "a0",
|
||||
"seed": 739933670,
|
||||
"children": {
|
||||
"affine:surface:ymap": true,
|
||||
"json": {
|
||||
"BvanBL7O38": {
|
||||
"index": "a0"
|
||||
},
|
||||
"QkmFfps45U": {
|
||||
"index": "a0",
|
||||
"parent": "BvanBL7O38"
|
||||
},
|
||||
"1IN8YOdsCP": {
|
||||
"index": "a1",
|
||||
"parent": "BvanBL7O38"
|
||||
},
|
||||
"iFQ9oVR0KN": {
|
||||
"index": "a2",
|
||||
"parent": "BvanBL7O38"
|
||||
}
|
||||
}
|
||||
},
|
||||
"layoutType": 0,
|
||||
"style": 1,
|
||||
"type": "mindmap",
|
||||
"id": "MYyOCcLTWN"
|
||||
},
|
||||
"BvanBL7O38": {
|
||||
"index": "a0",
|
||||
"seed": 819595867,
|
||||
"color": "--affine-black",
|
||||
"fillColor": "--affine-white",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Poppins",
|
||||
"fontSize": 20,
|
||||
"fontWeight": "600",
|
||||
"maxWidth": 400,
|
||||
"padding": [11, 22],
|
||||
"radius": 8,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": {
|
||||
"offsetX": 0,
|
||||
"offsetY": 6,
|
||||
"blur": 12,
|
||||
"color": "rgba(0, 0, 0, 0.14)"
|
||||
},
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "#84CFFF",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 4,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Mind Map"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 0,
|
||||
"xywh": "[-214.85955810546875,-113.65847778320312,144.7799530029297,52]",
|
||||
"type": "shape",
|
||||
"id": "BvanBL7O38"
|
||||
},
|
||||
"QkmFfps45U": {
|
||||
"index": "a0",
|
||||
"seed": 557026939,
|
||||
"color": "--affine-black",
|
||||
"fillColor": "--affine-white",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Poppins",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "500",
|
||||
"maxWidth": false,
|
||||
"padding": [6, 22],
|
||||
"radius": 8,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": {
|
||||
"offsetX": 0,
|
||||
"offsetY": 6,
|
||||
"blur": 12,
|
||||
"color": "rgba(0, 0, 0, 0.14)"
|
||||
},
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-purple",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 3,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 0,
|
||||
"xywh": "[129.92039489746094,-186.65847778320312,76.83197021484375,36]",
|
||||
"type": "shape",
|
||||
"id": "QkmFfps45U"
|
||||
},
|
||||
"1IN8YOdsCP": {
|
||||
"index": "a0",
|
||||
"seed": 205695803,
|
||||
"color": "--affine-black",
|
||||
"fillColor": "--affine-white",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Poppins",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "500",
|
||||
"maxWidth": false,
|
||||
"padding": [6, 22],
|
||||
"radius": 8,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": {
|
||||
"offsetX": 0,
|
||||
"offsetY": 6,
|
||||
"blur": 12,
|
||||
"color": "rgba(0, 0, 0, 0.14)"
|
||||
},
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-magenta",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 3,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 0,
|
||||
"xywh": "[129.92039489746094,-105.65847778320312,76.83197021484375,36]",
|
||||
"type": "shape",
|
||||
"id": "1IN8YOdsCP"
|
||||
},
|
||||
"iFQ9oVR0KN": {
|
||||
"index": "a0",
|
||||
"seed": 585656351,
|
||||
"color": "--affine-black",
|
||||
"fillColor": "--affine-white",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Poppins",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "500",
|
||||
"maxWidth": false,
|
||||
"padding": [6, 22],
|
||||
"radius": 8,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": {
|
||||
"offsetX": 0,
|
||||
"offsetY": 6,
|
||||
"blur": 12,
|
||||
"color": "rgba(0, 0, 0, 0.14)"
|
||||
},
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-orange",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 3,
|
||||
"text": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"textResizing": 0,
|
||||
"xywh": "[129.92039489746094,-24.658477783203125,76.83197021484375,36]",
|
||||
"type": "shape",
|
||||
"id": "iFQ9oVR0KN"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "bVYX1z2q3T",
|
||||
"flavour": "affine:frame",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Frame 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": "--affine-palette-transparent",
|
||||
"xywh": "[-542.4695816040039,-292.0061340332031,798.42578125,414.50390625]",
|
||||
"index": "Zz",
|
||||
"childElementIds": {
|
||||
"MYyOCcLTWN": true
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
{
|
||||
"type": "page",
|
||||
"meta": {
|
||||
"id": "pfXt9oH_Rb",
|
||||
"title": "Note",
|
||||
"createDate": 1725451650158,
|
||||
"tags": []
|
||||
},
|
||||
"blocks": {
|
||||
"type": "block",
|
||||
"id": "hNFCyKIXqH",
|
||||
"flavour": "affine:page",
|
||||
"version": 2,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Note"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "odnFJlKwxt",
|
||||
"flavour": "affine:surface",
|
||||
"version": 5,
|
||||
"props": {
|
||||
"elements": {}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "D1UhpqzS37",
|
||||
"flavour": "affine:frame",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Frame 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": "--affine-palette-transparent",
|
||||
"xywh": "[361.3046875,-9.600906372070312,803.04296875,291.26953125]",
|
||||
"index": "a0",
|
||||
"childElementIds": {
|
||||
"C5e-RyxlTI": true
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "C5e-RyxlTI",
|
||||
"flavour": "affine:note",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"xywh": "[538.826171875,46.71159362792969,448,180]",
|
||||
"background": "--affine-note-background-blue",
|
||||
"index": "a3",
|
||||
"hidden": false,
|
||||
"displayMode": "edgeless",
|
||||
"edgeless": {
|
||||
"style": {
|
||||
"borderRadius": 0,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-sticker"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "AjSEjoCq1Q",
|
||||
"flavour": "affine:paragraph",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"type": "h1",
|
||||
"text": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Write, draw, plan all at once."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "s5v28mktAO",
|
||||
"flavour": "affine:paragraph",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"type": "text",
|
||||
"text": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "AFFiNE is a workspace with fully merged docs,"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "Jd9Unn0j4a",
|
||||
"flavour": "affine:paragraph",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"type": "text",
|
||||
"text": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "whiteboards and databases."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
-2192
File diff suppressed because it is too large
Load Diff
-179
@@ -1,179 +0,0 @@
|
||||
{
|
||||
"type": "page",
|
||||
"meta": {
|
||||
"id": "UpLwaWKe8S9s8krsxi3A8",
|
||||
"title": "BlockSuite Playground",
|
||||
"createDate": 1725435317986,
|
||||
"tags": []
|
||||
},
|
||||
"blocks": {
|
||||
"type": "block",
|
||||
"id": "mUEhyeRfVdzFKHV3w897g",
|
||||
"flavour": "affine:page",
|
||||
"version": 2,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "BlockSuite Playground"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "oc5oq3DFWcqVjzyevWrx6",
|
||||
"flavour": "affine:surface",
|
||||
"version": 5,
|
||||
"props": {
|
||||
"elements": {
|
||||
"e6t9tKz8Sy": {
|
||||
"index": "a5",
|
||||
"seed": 338503204,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "ellipse",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textResizing": 1,
|
||||
"xywh": "[190.275413673033,378.1994245551321,100.11936661988601,100.11932857954827]",
|
||||
"type": "shape",
|
||||
"id": "e6t9tKz8Sy"
|
||||
},
|
||||
"F8qB_zDC5Q": {
|
||||
"index": "a6",
|
||||
"seed": 1896265661,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0.1,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textResizing": 1,
|
||||
"xywh": "[717.3900484965246,378.29923435003775,100.25918467911882,99.76145851000166]",
|
||||
"type": "shape",
|
||||
"id": "F8qB_zDC5Q"
|
||||
},
|
||||
"mPR44JBpcd": {
|
||||
"index": "a7",
|
||||
"seed": 2073974140,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "diamond",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textResizing": 1,
|
||||
"xywh": "[364.3172984791928,376.3029173497159,105.1083470248982,104.78589902189475]",
|
||||
"type": "shape",
|
||||
"id": "mPR44JBpcd"
|
||||
},
|
||||
"cmtluc3FWR": {
|
||||
"index": "a8",
|
||||
"seed": 1457248130,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "triangle",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textResizing": 1,
|
||||
"xywh": "[543.3481636903648,382.77123494325747,99.95316838431143,93.33554571739376]",
|
||||
"type": "shape",
|
||||
"id": "cmtluc3FWR"
|
||||
},
|
||||
"knt_TKvACR": {
|
||||
"index": "a9",
|
||||
"seed": 1896265661,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 12,
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textResizing": 1,
|
||||
"xywh": "[16.1402039335359,378.4574848297732,100.2126915532233,99.60320803026627]",
|
||||
"type": "shape",
|
||||
"id": "knt_TKvACR"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "NSlTmqVn2NsIJCDvzIoUF",
|
||||
"flavour": "affine:frame",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Shapes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": "--affine-palette-transparent",
|
||||
"xywh": "[-23.859796066464128,332.87664265400565,881.5090292421075,190.55181066780085]",
|
||||
"index": "a0"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
{
|
||||
"type": "page",
|
||||
"meta": {
|
||||
"id": "NJwLoscdk4",
|
||||
"title": "BlockSuite Playground",
|
||||
"createDate": 1725610138401,
|
||||
"tags": []
|
||||
},
|
||||
"blocks": {
|
||||
"type": "block",
|
||||
"id": "W-A_1geJy3",
|
||||
"flavour": "affine:page",
|
||||
"version": 2,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "BlockSuite Playground"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "KNi1ipoyX6",
|
||||
"flavour": "affine:surface",
|
||||
"version": 5,
|
||||
"props": {
|
||||
"elements": {}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "bv-d4LA-Nr",
|
||||
"flavour": "affine:edgeless-text",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"xywh": "[18.221401559354234,201.98389611782602,800.6283921393511,489.15472412109375]",
|
||||
"index": "a0",
|
||||
"color": "--affine-palette-line-blue",
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontStyle": "normal",
|
||||
"fontWeight": "400",
|
||||
"textAlign": "left",
|
||||
"scale": 5.435052533564726,
|
||||
"rotate": 0,
|
||||
"hasMaxWidth": true
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "vkzwo90EHS",
|
||||
"flavour": "affine:paragraph",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"type": "text",
|
||||
"text": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "To Shape,"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "0ugn1XiO-U",
|
||||
"flavour": "affine:paragraph",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"type": "text",
|
||||
"text": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Not to Adopt."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "mkbmK_R1sC",
|
||||
"flavour": "affine:frame",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Frame 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": "--affine-palette-transparent",
|
||||
"xywh": "[-21.778598440645766,126.5612581783729,880.6283921393511,640]",
|
||||
"index": "a1",
|
||||
"childElementIds": {
|
||||
"bv-d4LA-Nr": true
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { SettingWrapper } from '@affine/component/setting-components';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
|
||||
import { ConnectorSettings } from './connector';
|
||||
import { GeneralEdgelessSetting } from './general';
|
||||
import { MindMapSettings } from './mind-map';
|
||||
import { NoteSettings } from './note';
|
||||
import { PenSettings } from './pen';
|
||||
import { ShapeSettings } from './shape';
|
||||
import { TextSettings } from './text';
|
||||
|
||||
export const Edgeless = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<SettingWrapper title={t['com.affine.settings.editorSettings.edgeless']()}>
|
||||
<GeneralEdgelessSetting />
|
||||
<NoteSettings />
|
||||
<TextSettings />
|
||||
<ShapeSettings />
|
||||
<ConnectorSettings />
|
||||
<PenSettings />
|
||||
<MindMapSettings />
|
||||
</SettingWrapper>
|
||||
);
|
||||
};
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import type { EdgelessDefaultTheme } from '@affine/core/modules/editor-setting/schema';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { menuTrigger } from '../style.css';
|
||||
|
||||
const getThemeOptions = (
|
||||
t: ReturnType<typeof useI18n>
|
||||
): { value: EdgelessDefaultTheme; label: string }[] => [
|
||||
{
|
||||
value: 'specified' as EdgelessDefaultTheme,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.page.edgeless-default-theme.specified'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: 'dark' as EdgelessDefaultTheme,
|
||||
label: t['com.affine.themeSettings.dark'](),
|
||||
},
|
||||
{
|
||||
value: 'light' as EdgelessDefaultTheme,
|
||||
label: t['com.affine.themeSettings.light'](),
|
||||
},
|
||||
{
|
||||
value: 'auto' as EdgelessDefaultTheme,
|
||||
label: t['com.affine.themeSettings.auto'](),
|
||||
},
|
||||
];
|
||||
|
||||
const getThemeValue = (
|
||||
theme: EdgelessDefaultTheme,
|
||||
t: ReturnType<typeof useI18n>
|
||||
) => {
|
||||
switch (theme) {
|
||||
case 'dark':
|
||||
return t['com.affine.themeSettings.dark']();
|
||||
case 'light':
|
||||
return t['com.affine.themeSettings.light']();
|
||||
case 'auto':
|
||||
return t['com.affine.themeSettings.auto']();
|
||||
case 'specified':
|
||||
return t[
|
||||
'com.affine.settings.editorSettings.page.edgeless-default-theme.specified'
|
||||
]();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const GeneralEdgelessSetting = () => {
|
||||
const t = useI18n();
|
||||
const editorSetting = useService(EditorSettingService).editorSetting;
|
||||
const edgelessDefaultTheme = useLiveData(
|
||||
editorSetting.settings$
|
||||
).edgelessDefaultTheme;
|
||||
|
||||
const items = getThemeOptions(t);
|
||||
const currentTheme = useMemo(() => {
|
||||
return getThemeValue(edgelessDefaultTheme, t);
|
||||
}, [edgelessDefaultTheme, t]);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
return items.map(item => {
|
||||
const selected = edgelessDefaultTheme === item.value;
|
||||
const onSelect = () => {
|
||||
editorSetting.set('edgelessDefaultTheme', item.value);
|
||||
};
|
||||
return (
|
||||
<MenuItem key={item.value} selected={selected} onSelect={onSelect}>
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, items, edgelessDefaultTheme]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.page.edgeless-default-theme.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.page.edgeless-default-theme.description'
|
||||
]()}
|
||||
>
|
||||
<Menu
|
||||
items={menuItems}
|
||||
contentOptions={{
|
||||
align: 'end',
|
||||
sideOffset: 16,
|
||||
style: {
|
||||
width: '280px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuTrigger tooltip={currentTheme} className={menuTrigger}>
|
||||
{currentTheme}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export * from './edgeless';
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
import {
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
RadioGroup,
|
||||
type RadioItem,
|
||||
} from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { LayoutType, MindmapStyle } from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { useFramework, useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { DropdownMenu } from '../menu';
|
||||
import { menuTrigger, settingWrapper } from '../style.css';
|
||||
import { EdgelessSnapshot } from './snapshot';
|
||||
import { getSurfaceBlock } from './utils';
|
||||
|
||||
const MINDMAP_STYLES = [
|
||||
{
|
||||
value: MindmapStyle.ONE,
|
||||
name: 'Style 1',
|
||||
},
|
||||
{
|
||||
value: MindmapStyle.TWO,
|
||||
name: 'Style 2',
|
||||
},
|
||||
{
|
||||
value: MindmapStyle.THREE,
|
||||
name: 'Style 3',
|
||||
},
|
||||
{
|
||||
value: MindmapStyle.FOUR,
|
||||
name: 'Style 4',
|
||||
},
|
||||
];
|
||||
|
||||
export const MindMapSettings = () => {
|
||||
const t = useI18n();
|
||||
const framework = useFramework();
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
const settings = useLiveData(editorSetting.settings$);
|
||||
|
||||
const { layoutType } = settings.mindmap;
|
||||
const setLayoutType = useCallback(
|
||||
(value: LayoutType) => {
|
||||
editorSetting.set('mindmap', {
|
||||
layoutType: value,
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
const layoutTypeItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: LayoutType.LEFT as any,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.mind-map.layout.left'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: LayoutType.BALANCE as any,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.mind-map.layout.radial'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: LayoutType.RIGHT as any,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.mind-map.layout.right'
|
||||
](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const styleItems = useMemo(() => {
|
||||
const { style } = settings.mindmap;
|
||||
return MINDMAP_STYLES.map(({ name, value }) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('mindmap', { style: value });
|
||||
};
|
||||
const isSelected = style === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const getElements = useCallback((doc: Doc) => {
|
||||
const surface = getSurfaceBlock(doc);
|
||||
return surface?.getElementsByType('mindmap') || [];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EdgelessSnapshot
|
||||
title={t['com.affine.settings.editorSettings.edgeless.mind-map']()}
|
||||
docName="mindmap"
|
||||
keyName={'mindmap' as any}
|
||||
getElements={getElements}
|
||||
height={320}
|
||||
/>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.style']()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={styleItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{`Style ${settings.mindmap.style}`}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.mind-map.layout'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={layoutTypeItems}
|
||||
value={layoutType}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setLayoutType}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-264
@@ -1,264 +0,0 @@
|
||||
import {
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
RadioGroup,
|
||||
type RadioItem,
|
||||
Slider,
|
||||
} from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
createEnumMap,
|
||||
NoteBackgroundColor,
|
||||
NoteBackgroundColorMap,
|
||||
NoteShadow,
|
||||
NoteShadowMap,
|
||||
StrokeStyle,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { useFramework, useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { DropdownMenu } from '../menu';
|
||||
import { menuTrigger, settingWrapper } from '../style.css';
|
||||
import { useColor } from '../utils';
|
||||
import { Point } from './point';
|
||||
import { EdgelessSnapshot } from './snapshot';
|
||||
|
||||
enum CornerSize {
|
||||
None = 0,
|
||||
Small = 8,
|
||||
Medium = 16,
|
||||
Large = 24,
|
||||
Huge = 32,
|
||||
}
|
||||
|
||||
const CornerSizeMap = createEnumMap(CornerSize);
|
||||
|
||||
const CORNER_SIZE = [
|
||||
{ name: 'None', value: CornerSize.None },
|
||||
{ name: 'Small', value: CornerSize.Small },
|
||||
{ name: 'Medium', value: CornerSize.Medium },
|
||||
{ name: 'Large', value: CornerSize.Large },
|
||||
{ name: 'Huge', value: CornerSize.Huge },
|
||||
] as const;
|
||||
|
||||
export const NoteSettings = () => {
|
||||
const t = useI18n();
|
||||
const framework = useFramework();
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
const settings = useLiveData(editorSetting.settings$);
|
||||
const getColorFromMap = useColor();
|
||||
|
||||
const borderStyleItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: StrokeStyle.Solid,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
|
||||
},
|
||||
{
|
||||
value: StrokeStyle.Dash,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
|
||||
},
|
||||
{
|
||||
value: StrokeStyle.None,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.note.border.none'](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const { borderStyle } = settings['affine:note'].edgeless.style;
|
||||
const setBorderStyle = useCallback(
|
||||
(value: StrokeStyle) => {
|
||||
editorSetting.set('affine:note', {
|
||||
edgeless: {
|
||||
style: {
|
||||
borderStyle: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const { borderSize } = settings['affine:note'].edgeless.style;
|
||||
const setBorderSize = useCallback(
|
||||
(value: number[]) => {
|
||||
editorSetting.set('affine:note', {
|
||||
edgeless: {
|
||||
style: {
|
||||
borderSize: value[0],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const backgroundItems = useMemo(() => {
|
||||
const { background } = settings['affine:note'];
|
||||
return Object.entries(NoteBackgroundColor).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('affine:note', { background: value });
|
||||
};
|
||||
const isSelected = background === value;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
onSelect={handler}
|
||||
selected={isSelected}
|
||||
prefix={<Point color={value} />}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const cornerItems = useMemo(() => {
|
||||
const { borderRadius } = settings['affine:note'].edgeless.style;
|
||||
return CORNER_SIZE.map(({ name, value }) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('affine:note', {
|
||||
edgeless: {
|
||||
style: {
|
||||
borderRadius: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
const isSelected = borderRadius === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const shadowItems = useMemo(() => {
|
||||
const { shadowType } = settings['affine:note'].edgeless.style;
|
||||
return Object.entries(NoteShadow).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('affine:note', {
|
||||
edgeless: {
|
||||
style: {
|
||||
shadowType: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
const isSelected = shadowType === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const currentColor = useMemo(() => {
|
||||
const { background } = settings['affine:note'];
|
||||
return getColorFromMap(background, NoteBackgroundColorMap);
|
||||
}, [getColorFromMap, settings]);
|
||||
|
||||
const getElements = useCallback((doc: Doc) => {
|
||||
return doc.getBlocksByFlavour('affine:note') || [];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EdgelessSnapshot
|
||||
title={t['com.affine.settings.editorSettings.edgeless.note']()}
|
||||
docName="note"
|
||||
keyName="affine:note"
|
||||
getElements={getElements}
|
||||
height={240}
|
||||
/>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.note.background'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
{currentColor ? (
|
||||
<DropdownMenu
|
||||
items={backgroundItems}
|
||||
trigger={
|
||||
<MenuTrigger
|
||||
className={menuTrigger}
|
||||
prefix={<Point color={currentColor.value} />}
|
||||
>
|
||||
{currentColor.key}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.note.corners']()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={cornerItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{
|
||||
CornerSizeMap[
|
||||
settings['affine:note'].edgeless.style
|
||||
.borderRadius as CornerSize
|
||||
]
|
||||
}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.note.shadow']()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={shadowItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{NoteShadowMap[settings['affine:note'].edgeless.style.shadowType]}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.note.border']()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={borderStyleItems}
|
||||
value={borderStyle}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setBorderStyle}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.note.border-thickness'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<Slider
|
||||
value={[borderSize]}
|
||||
onValueChange={setBorderSize}
|
||||
min={2}
|
||||
max={12}
|
||||
step={2}
|
||||
nodes={[2, 4, 6, 8, 10, 12]}
|
||||
disabled={borderStyle === StrokeStyle.None}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
import { MenuItem, MenuTrigger, Slider } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { LineColor, LineColorMap } from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { useFramework, useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { DropdownMenu } from '../menu';
|
||||
import { menuTrigger } from '../style.css';
|
||||
import { useColor } from '../utils';
|
||||
import { Point } from './point';
|
||||
import { EdgelessSnapshot } from './snapshot';
|
||||
import { getSurfaceBlock } from './utils';
|
||||
|
||||
export const PenSettings = () => {
|
||||
const t = useI18n();
|
||||
const framework = useFramework();
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
const settings = useLiveData(editorSetting.settings$);
|
||||
const getColorFromMap = useColor();
|
||||
|
||||
const currentColor = useMemo(() => {
|
||||
return getColorFromMap(settings.brush.color, LineColorMap);
|
||||
}, [getColorFromMap, settings.brush.color]);
|
||||
|
||||
const colorItems = useMemo(() => {
|
||||
const { color } = settings.brush;
|
||||
return Object.entries(LineColor).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('brush', { color: value });
|
||||
};
|
||||
const isSelected = color === value;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
onSelect={handler}
|
||||
selected={isSelected}
|
||||
prefix={<Point color={value} />}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings.brush]);
|
||||
|
||||
const borderThickness = settings.brush.lineWidth;
|
||||
const setBorderThickness = useCallback(
|
||||
(value: number[]) => {
|
||||
editorSetting.set('brush', {
|
||||
lineWidth: value[0],
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const getElements = useCallback((doc: Doc) => {
|
||||
const surface = getSurfaceBlock(doc);
|
||||
return surface?.getElementsByType('brush') || [];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EdgelessSnapshot
|
||||
title={t['com.affine.settings.editorSettings.edgeless.pen']()}
|
||||
docName="pen"
|
||||
keyName="brush"
|
||||
getElements={getElements}
|
||||
/>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.pen.color']()}
|
||||
desc={''}
|
||||
>
|
||||
{currentColor ? (
|
||||
<DropdownMenu
|
||||
items={colorItems}
|
||||
trigger={
|
||||
<MenuTrigger
|
||||
className={menuTrigger}
|
||||
prefix={<Point color={currentColor.value} />}
|
||||
>
|
||||
{currentColor.key}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.pen.thickness']()}
|
||||
desc={''}
|
||||
>
|
||||
<Slider
|
||||
value={[borderThickness]}
|
||||
onValueChange={setBorderThickness}
|
||||
min={2}
|
||||
max={12}
|
||||
step={2}
|
||||
nodes={[2, 4, 6, 8, 10, 12]}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
|
||||
export const Point = ({
|
||||
color,
|
||||
size = 8,
|
||||
}: {
|
||||
color: string;
|
||||
size?: number;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: `var(${color})`,
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/blackBorder')}`,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
-598
@@ -1,598 +0,0 @@
|
||||
import {
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
RadioGroup,
|
||||
type RadioItem,
|
||||
Slider,
|
||||
} from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
EdgelessRootService,
|
||||
ShapeElementModel,
|
||||
ShapeName,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
createEnumMap,
|
||||
FontFamily,
|
||||
FontFamilyMap,
|
||||
FontStyle,
|
||||
FontWeightMap,
|
||||
getShapeName,
|
||||
LineColor,
|
||||
LineColorMap,
|
||||
ShapeFillColor,
|
||||
ShapeStyle,
|
||||
ShapeType,
|
||||
StrokeStyle,
|
||||
TextAlign,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { useFramework, useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { DropdownMenu } from '../menu';
|
||||
import {
|
||||
menuTrigger,
|
||||
preViewLabelWrapper,
|
||||
settingWrapper,
|
||||
shapeIndicator,
|
||||
} from '../style.css';
|
||||
import { sortedFontWeightEntries, useColor } from '../utils';
|
||||
import type { DocName } from './docs';
|
||||
import { Point } from './point';
|
||||
import { EdgelessSnapshot } from './snapshot';
|
||||
import { getSurfaceBlock } from './utils';
|
||||
|
||||
enum ShapeTextFontSize {
|
||||
'16px' = '16',
|
||||
'20px' = '20',
|
||||
'24px' = '24',
|
||||
'32px' = '32',
|
||||
'40px' = '40',
|
||||
'64px' = '64',
|
||||
}
|
||||
|
||||
const ShapeFillColorMap = createEnumMap(ShapeFillColor);
|
||||
|
||||
export const ShapeSettings = () => {
|
||||
const t = useI18n();
|
||||
const framework = useFramework();
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
const settings = useLiveData(editorSetting.settings$);
|
||||
const getColorFromMap = useColor();
|
||||
|
||||
const [currentShape, setCurrentShape] = useState<ShapeName>(ShapeType.Rect);
|
||||
|
||||
const shapeStyleItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: ShapeStyle.General,
|
||||
label: t['com.affine.settings.editorSettings.edgeless.style.general'](),
|
||||
},
|
||||
{
|
||||
value: ShapeStyle.Scribbled,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.style.scribbled'](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const { shapeStyle } = settings[`shape:${currentShape}`];
|
||||
const setShapeStyle = useCallback(
|
||||
(value: ShapeStyle) => {
|
||||
editorSetting.set(`shape:${currentShape}`, {
|
||||
shapeStyle: value,
|
||||
});
|
||||
},
|
||||
[editorSetting, currentShape]
|
||||
);
|
||||
|
||||
const borderStyleItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: StrokeStyle.Solid,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
|
||||
},
|
||||
{
|
||||
value: StrokeStyle.Dash,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
|
||||
},
|
||||
{
|
||||
value: StrokeStyle.None,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.note.border.none'](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const borderStyle = settings[`shape:${currentShape}`].strokeStyle;
|
||||
const setBorderStyle = useCallback(
|
||||
(value: StrokeStyle) => {
|
||||
editorSetting.set(`shape:${currentShape}`, {
|
||||
strokeStyle: value,
|
||||
});
|
||||
},
|
||||
[editorSetting, currentShape]
|
||||
);
|
||||
|
||||
const alignItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: TextAlign.Left,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.left'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: TextAlign.Center,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.center'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: TextAlign.Right,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.right'
|
||||
](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const textAlignment = settings[`shape:${currentShape}`].textAlign;
|
||||
const setTextAlignment = useCallback(
|
||||
(value: TextAlign) => {
|
||||
editorSetting.set(`shape:${currentShape}`, {
|
||||
textAlign: value,
|
||||
});
|
||||
},
|
||||
[editorSetting, currentShape]
|
||||
);
|
||||
|
||||
const shapes = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: ShapeType.Rect,
|
||||
label: t['com.affine.settings.editorSettings.edgeless.shape.square'](),
|
||||
},
|
||||
{
|
||||
value: ShapeType.Ellipse,
|
||||
label: t['com.affine.settings.editorSettings.edgeless.shape.ellipse'](),
|
||||
},
|
||||
{
|
||||
value: ShapeType.Diamond,
|
||||
label: t['com.affine.settings.editorSettings.edgeless.shape.diamond'](),
|
||||
},
|
||||
{
|
||||
value: ShapeType.Triangle,
|
||||
label:
|
||||
t['com.affine.settings.editorSettings.edgeless.shape.triangle'](),
|
||||
},
|
||||
{
|
||||
value: 'roundedRect',
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.rounded-rectangle'
|
||||
](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const docs = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: 'shape',
|
||||
label: t['com.affine.settings.editorSettings.edgeless.shape.list'](),
|
||||
},
|
||||
{
|
||||
value: 'flow',
|
||||
label: t['com.affine.settings.editorSettings.edgeless.shape.flow'](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
const [currentDoc, setCurrentDoc] = useState<DocName>('shape');
|
||||
|
||||
const fillColorItems = useMemo(() => {
|
||||
const { fillColor } = settings[`shape:${currentShape}`];
|
||||
return Object.entries(ShapeFillColor).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set(`shape:${currentShape}`, { fillColor: value });
|
||||
};
|
||||
const isSelected = fillColor === value;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
onSelect={handler}
|
||||
selected={isSelected}
|
||||
prefix={<Point color={value} />}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, currentShape]);
|
||||
|
||||
const borderColorItems = useMemo(() => {
|
||||
const { strokeColor } = settings[`shape:${currentShape}`];
|
||||
return Object.entries(LineColor).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set(`shape:${currentShape}`, { strokeColor: value });
|
||||
};
|
||||
const isSelected = strokeColor === value;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
onSelect={handler}
|
||||
selected={isSelected}
|
||||
prefix={<Point color={value} />}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, currentShape]);
|
||||
|
||||
const borderThickness = settings[`shape:${currentShape}`].strokeWidth;
|
||||
const setBorderThickness = useCallback(
|
||||
(value: number[]) => {
|
||||
editorSetting.set(`shape:${currentShape}`, {
|
||||
strokeWidth: value[0],
|
||||
});
|
||||
},
|
||||
[editorSetting, currentShape]
|
||||
);
|
||||
|
||||
const fontFamilyItems = useMemo(() => {
|
||||
const { fontFamily } = settings[`shape:${currentShape}`];
|
||||
return Object.entries(FontFamily).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set(`shape:${currentShape}`, { fontFamily: value });
|
||||
};
|
||||
const isSelected = fontFamily === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, currentShape]);
|
||||
|
||||
const fontStyleItems = useMemo(() => {
|
||||
const { fontStyle } = settings[`shape:${currentShape}`];
|
||||
return Object.entries(FontStyle).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set(`shape:${currentShape}`, { fontStyle: value });
|
||||
};
|
||||
const isSelected = fontStyle === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, currentShape]);
|
||||
|
||||
const fontWeightItems = useMemo(() => {
|
||||
const { fontWeight } = settings[`shape:${currentShape}`];
|
||||
return sortedFontWeightEntries.map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set(`shape:${currentShape}`, { fontWeight: value });
|
||||
};
|
||||
const isSelected = fontWeight === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, currentShape]);
|
||||
|
||||
const fontSizeItems = useMemo(() => {
|
||||
const { fontSize } = settings[`shape:${currentShape}`];
|
||||
return Object.entries(ShapeTextFontSize).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set(`shape:${currentShape}`, { fontSize: Number(value) });
|
||||
};
|
||||
const isSelected = fontSize === Number(value);
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, currentShape]);
|
||||
|
||||
const textColorItems = useMemo(() => {
|
||||
const { color } = settings[`shape:${currentShape}`];
|
||||
return Object.entries(LineColor).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set(`shape:${currentShape}`, { color: value });
|
||||
};
|
||||
const isSelected = color === value;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
onSelect={handler}
|
||||
selected={isSelected}
|
||||
prefix={<Point color={value} />}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, currentShape]);
|
||||
|
||||
const getElements = useCallback(
|
||||
(doc: Doc) => {
|
||||
const surface = getSurfaceBlock(doc);
|
||||
if (!surface) return [];
|
||||
return surface.getElementsByType('shape').filter(node => {
|
||||
const shape = node as ShapeElementModel;
|
||||
const { shapeType, radius } = shape;
|
||||
const shapeName = getShapeName(shapeType, radius);
|
||||
return shapeName === currentShape;
|
||||
});
|
||||
},
|
||||
[currentShape]
|
||||
);
|
||||
|
||||
const firstUpdate = useCallback(
|
||||
(doc: Doc, editorHost: EditorHost) => {
|
||||
const edgelessService = editorHost.std.getService(
|
||||
'affine:page'
|
||||
) as EdgelessRootService;
|
||||
const surface = getSurfaceBlock(doc);
|
||||
if (!surface) return;
|
||||
surface.getElementsByType('shape').forEach(node => {
|
||||
const shape = node as ShapeElementModel;
|
||||
const { shapeType, radius } = shape;
|
||||
const shapeName = getShapeName(shapeType, radius);
|
||||
const props = editorSetting.get(`shape:${shapeName}`);
|
||||
edgelessService.updateElement(shape.id, props);
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const fillColor = useMemo(() => {
|
||||
const color = settings[`shape:${currentShape}`].fillColor;
|
||||
return getColorFromMap(color, ShapeFillColorMap);
|
||||
}, [currentShape, getColorFromMap, settings]);
|
||||
|
||||
const borderColor = useMemo(() => {
|
||||
const color = settings[`shape:${currentShape}`].strokeColor;
|
||||
return getColorFromMap(color, LineColorMap);
|
||||
}, [currentShape, getColorFromMap, settings]);
|
||||
|
||||
const textColor = useMemo(() => {
|
||||
const color = settings[`shape:${currentShape}`].color;
|
||||
return getColorFromMap(color, LineColorMap);
|
||||
}, [currentShape, getColorFromMap, settings]);
|
||||
|
||||
const height = currentDoc === 'flow' ? 456 : 180;
|
||||
return (
|
||||
<>
|
||||
<EdgelessSnapshot
|
||||
key={currentDoc}
|
||||
title={t['com.affine.settings.editorSettings.edgeless.shape']()}
|
||||
docName={currentDoc}
|
||||
keyName={`shape:${currentShape}`}
|
||||
height={height}
|
||||
getElements={getElements}
|
||||
firstUpdate={firstUpdate}
|
||||
>
|
||||
<RadioGroup
|
||||
value={currentDoc}
|
||||
items={docs}
|
||||
onChange={setCurrentDoc}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '10px',
|
||||
bottom: '10px',
|
||||
}}
|
||||
className={preViewLabelWrapper}
|
||||
/>
|
||||
</EdgelessSnapshot>
|
||||
|
||||
<RadioGroup
|
||||
padding={0}
|
||||
gap={4}
|
||||
itemHeight={28}
|
||||
borderRadius={8}
|
||||
value={currentShape}
|
||||
items={shapes}
|
||||
onChange={setCurrentShape}
|
||||
style={{ background: 'transparent', marginBottom: '16px' }}
|
||||
indicatorClassName={shapeIndicator}
|
||||
/>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.style']()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={shapeStyleItems}
|
||||
value={shapeStyle}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setShapeStyle}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.fill-color'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
{fillColor ? (
|
||||
<DropdownMenu
|
||||
items={fillColorItems}
|
||||
trigger={
|
||||
<MenuTrigger
|
||||
className={menuTrigger}
|
||||
prefix={<Point color={fillColor.value} />}
|
||||
>
|
||||
{fillColor.key}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.border-color'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
{borderColor ? (
|
||||
<DropdownMenu
|
||||
items={borderColorItems}
|
||||
trigger={
|
||||
<MenuTrigger
|
||||
className={menuTrigger}
|
||||
prefix={<Point color={borderColor.value} />}
|
||||
>
|
||||
{borderColor.key}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.border-style'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={borderStyleItems}
|
||||
value={borderStyle}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setBorderStyle}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.border-thickness'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<Slider
|
||||
value={[borderThickness]}
|
||||
onValueChange={setBorderThickness}
|
||||
min={2}
|
||||
max={12}
|
||||
step={2}
|
||||
nodes={[2, 4, 6, 8, 10, 12]}
|
||||
disabled={borderStyle === StrokeStyle.None}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.text-color'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
{textColor ? (
|
||||
<DropdownMenu
|
||||
items={textColorItems}
|
||||
trigger={
|
||||
<MenuTrigger
|
||||
className={menuTrigger}
|
||||
prefix={<Point color={textColor.value} />}
|
||||
>
|
||||
{textColor.key}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-family'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontFamilyItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{FontFamilyMap[settings[`shape:${currentShape}`].fontFamily]}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.font-size'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontSizeItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{settings[`shape:${currentShape}`].fontSize + 'px'}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-style'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontStyleItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{settings[`shape:${currentShape}`].fontStyle}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-weight'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontWeightItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{FontWeightMap[settings[`shape:${currentShape}`].fontWeight]}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.shape.text-alignment'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={alignItems}
|
||||
value={textAlignment}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setTextAlignment}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-155
@@ -1,155 +0,0 @@
|
||||
import { Skeleton } from '@affine/component';
|
||||
import type { EditorSettingSchema } from '@affine/core/modules/editor-setting';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||
import { BlockStdScope } from '@blocksuite/affine/block-std';
|
||||
import type { GfxPrimitiveElementModel } from '@blocksuite/affine/block-std/gfx';
|
||||
import type { EdgelessRootService } from '@blocksuite/affine/blocks';
|
||||
import { SpecProvider } from '@blocksuite/affine/blocks';
|
||||
import { Bound } from '@blocksuite/affine/global/utils';
|
||||
import type { Block, Doc } from '@blocksuite/affine/store';
|
||||
import { useFramework } from '@toeverything/infra';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { map, pairwise } from 'rxjs';
|
||||
|
||||
import {
|
||||
snapshotContainer,
|
||||
snapshotLabel,
|
||||
snapshotSkeleton,
|
||||
snapshotTitle,
|
||||
} from '../style.css';
|
||||
import { type DocName, getDocByName } from './docs';
|
||||
import { getFrameBlock } from './utils';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
docName: DocName;
|
||||
keyName: keyof EditorSettingSchema;
|
||||
height?: number;
|
||||
getElements: (doc: Doc) => Array<Block | GfxPrimitiveElementModel>;
|
||||
firstUpdate?: (doc: Doc, editorHost: EditorHost) => void;
|
||||
children?: React.ReactElement;
|
||||
}
|
||||
|
||||
const boundMap = new Map<DocName, Bound>();
|
||||
|
||||
export const EdgelessSnapshot = (props: Props) => {
|
||||
const {
|
||||
title,
|
||||
docName,
|
||||
keyName,
|
||||
height = 180,
|
||||
getElements,
|
||||
firstUpdate,
|
||||
children,
|
||||
} = props;
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const docRef = useRef<Doc | null>(null);
|
||||
const editorHostRef = useRef<EditorHost | null>(null);
|
||||
const framework = useFramework();
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
|
||||
const updateElements = useCallback(() => {
|
||||
const editorHost = editorHostRef.current;
|
||||
const doc = docRef.current;
|
||||
if (!editorHost || !doc) return;
|
||||
const edgelessService = editorHost.std.getService(
|
||||
'affine:page'
|
||||
) as EdgelessRootService;
|
||||
const elements = getElements(doc);
|
||||
const props = editorSetting.get(keyName) as any;
|
||||
elements.forEach(element => {
|
||||
edgelessService.updateElement(element.id, props);
|
||||
});
|
||||
}, [editorSetting, getElements, keyName]);
|
||||
|
||||
const renderEditor = useCallback(async () => {
|
||||
if (!wrapperRef.current) return;
|
||||
const doc = await getDocByName(docName);
|
||||
if (!doc) return;
|
||||
|
||||
const editorHost = new BlockStdScope({
|
||||
doc,
|
||||
extensions: SpecProvider.getInstance().getSpec('edgeless:preview').value,
|
||||
}).render();
|
||||
docRef.current = doc;
|
||||
editorHostRef.current = editorHost;
|
||||
|
||||
if (firstUpdate) {
|
||||
firstUpdate(doc, editorHost);
|
||||
} else {
|
||||
updateElements();
|
||||
}
|
||||
|
||||
// refresh viewport
|
||||
const edgelessService = editorHost.std.getService(
|
||||
'affine:page'
|
||||
) as EdgelessRootService;
|
||||
edgelessService.specSlots.viewConnected.once(({ component }) => {
|
||||
const edgelessBlock = component as any;
|
||||
edgelessBlock.editorViewportSelector = 'ref-viewport';
|
||||
const frame = getFrameBlock(doc);
|
||||
if (frame) {
|
||||
boundMap.set(docName, Bound.deserialize(frame.xywh));
|
||||
doc.deleteBlock(frame);
|
||||
}
|
||||
const bound = boundMap.get(docName);
|
||||
bound && edgelessService.viewport.setViewportByBound(bound);
|
||||
});
|
||||
|
||||
// append to dom node
|
||||
wrapperRef.current.append(editorHost);
|
||||
}, [docName, firstUpdate, updateElements]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
renderEditor();
|
||||
return () => editorHostRef.current?.remove();
|
||||
}, [renderEditor]);
|
||||
|
||||
// observe editor settings change
|
||||
useEffect(() => {
|
||||
const sub = editorSetting.provider
|
||||
.watchAll()
|
||||
.pipe(
|
||||
map(settings => {
|
||||
if (typeof settings[keyName] === 'string') {
|
||||
return JSON.parse(settings[keyName]);
|
||||
}
|
||||
return keyName;
|
||||
}),
|
||||
pairwise()
|
||||
)
|
||||
.subscribe(([prev, current]) => {
|
||||
if (!isEqual(prev, current)) {
|
||||
updateElements();
|
||||
}
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
}, [editorSetting.provider, keyName, updateElements]);
|
||||
|
||||
return (
|
||||
<div className={snapshotContainer}>
|
||||
<div className={snapshotTitle}>{title}</div>
|
||||
<div className={snapshotLabel}>{title}</div>
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
overflow: 'hidden',
|
||||
height,
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
className={snapshotSkeleton}
|
||||
variant="rounded"
|
||||
height={'100%'}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-232
@@ -1,232 +0,0 @@
|
||||
import {
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
RadioGroup,
|
||||
type RadioItem,
|
||||
} from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
FontFamily,
|
||||
FontFamilyMap,
|
||||
FontStyle,
|
||||
FontWeightMap,
|
||||
LineColor,
|
||||
LineColorMap,
|
||||
TextAlign,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { useFramework, useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { DropdownMenu } from '../menu';
|
||||
import { menuTrigger, settingWrapper } from '../style.css';
|
||||
import { sortedFontWeightEntries, useColor } from '../utils';
|
||||
import { Point } from './point';
|
||||
import { EdgelessSnapshot } from './snapshot';
|
||||
|
||||
export const TextSettings = () => {
|
||||
const t = useI18n();
|
||||
const framework = useFramework();
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
const settings = useLiveData(editorSetting.settings$);
|
||||
const getColorFromMap = useColor();
|
||||
|
||||
const alignItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: TextAlign.Left,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.left'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: TextAlign.Center,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.center'
|
||||
](),
|
||||
},
|
||||
{
|
||||
value: TextAlign.Right,
|
||||
label:
|
||||
t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.alignment.right'
|
||||
](),
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const { textAlign } = settings['affine:edgeless-text'];
|
||||
const setTextAlign = useCallback(
|
||||
(value: TextAlign) => {
|
||||
editorSetting.set('affine:edgeless-text', {
|
||||
textAlign: value,
|
||||
});
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
const colorItems = useMemo(() => {
|
||||
const { color } = settings['affine:edgeless-text'];
|
||||
return Object.entries(LineColor).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('affine:edgeless-text', { color: value });
|
||||
};
|
||||
const isSelected = color === value;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
onSelect={handler}
|
||||
selected={isSelected}
|
||||
prefix={<Point color={value} />}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const fontFamilyItems = useMemo(() => {
|
||||
const { fontFamily } = settings['affine:edgeless-text'];
|
||||
return Object.entries(FontFamily).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('affine:edgeless-text', { fontFamily: value });
|
||||
};
|
||||
const isSelected = fontFamily === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const fontStyleItems = useMemo(() => {
|
||||
const { fontStyle } = settings['affine:edgeless-text'];
|
||||
return Object.entries(FontStyle).map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('affine:edgeless-text', { fontStyle: value });
|
||||
};
|
||||
const isSelected = fontStyle === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const fontWeightItems = useMemo(() => {
|
||||
const { fontWeight } = settings['affine:edgeless-text'];
|
||||
return sortedFontWeightEntries.map(([name, value]) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('affine:edgeless-text', { fontWeight: value });
|
||||
};
|
||||
const isSelected = fontWeight === value;
|
||||
return (
|
||||
<MenuItem key={name} onSelect={handler} selected={isSelected}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings]);
|
||||
|
||||
const currentColor = useMemo(() => {
|
||||
const { color } = settings['affine:edgeless-text'];
|
||||
return getColorFromMap(color, LineColorMap);
|
||||
}, [getColorFromMap, settings]);
|
||||
|
||||
const getElements = useCallback((doc: Doc) => {
|
||||
return doc.getBlocksByFlavour('affine:edgeless-text') || [];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EdgelessSnapshot
|
||||
title={t['com.affine.settings.editorSettings.edgeless.text']()}
|
||||
docName="text"
|
||||
keyName="affine:edgeless-text"
|
||||
getElements={getElements}
|
||||
/>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.text.color']()}
|
||||
desc={''}
|
||||
>
|
||||
{currentColor ? (
|
||||
<DropdownMenu
|
||||
items={colorItems}
|
||||
trigger={
|
||||
<MenuTrigger
|
||||
className={menuTrigger}
|
||||
prefix={<Point color={currentColor.value} />}
|
||||
>
|
||||
{currentColor.key}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-family'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontFamilyItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{FontFamilyMap[settings['affine:edgeless-text'].fontFamily]}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-style'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontStyleItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{String(settings['affine:edgeless-text'].fontStyle)}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.edgeless.text.font-weight'
|
||||
]()}
|
||||
desc={''}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={fontWeightItems}
|
||||
trigger={
|
||||
<MenuTrigger className={menuTrigger}>
|
||||
{FontWeightMap[settings['affine:edgeless-text'].fontWeight]}
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.edgeless.text.alignment']()}
|
||||
desc={''}
|
||||
>
|
||||
<RadioGroup
|
||||
items={alignItems}
|
||||
value={textAlign}
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
onChange={setTextAlign}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import type { SurfaceBlockModel } from '@blocksuite/affine/block-std/gfx';
|
||||
import type { FrameBlockModel } from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
|
||||
export function getSurfaceBlock(doc: Doc) {
|
||||
const blocks = doc.getBlocksByFlavour('affine:surface');
|
||||
return blocks.length !== 0 ? (blocks[0].model as SurfaceBlockModel) : null;
|
||||
}
|
||||
|
||||
export function getFrameBlock(doc: Doc) {
|
||||
const blocks = doc.getBlocksByFlavour('affine:frame');
|
||||
return blocks.length !== 0 ? (blocks[0].model as FrameBlockModel) : null;
|
||||
}
|
||||
-477
@@ -1,477 +0,0 @@
|
||||
import {
|
||||
Loading,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
MenuTrigger,
|
||||
RadioGroup,
|
||||
type RadioItem,
|
||||
RowInput,
|
||||
Scrollable,
|
||||
Switch,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import {
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { ServerConfigService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
EditorSettingService,
|
||||
type FontFamily,
|
||||
fontStyleOptions,
|
||||
} from '@affine/core/modules/editor-setting';
|
||||
import {
|
||||
type FontData,
|
||||
SystemFontFamilyService,
|
||||
} from '@affine/core/modules/system-font-family';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import { DoneIcon, SearchIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
type PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import { DropdownMenu } from './menu';
|
||||
import * as styles from './style.css';
|
||||
|
||||
const getLabel = (fontKey: FontFamily, t: ReturnType<typeof useI18n>) => {
|
||||
switch (fontKey) {
|
||||
case 'Sans':
|
||||
return t['com.affine.appearanceSettings.fontStyle.sans']();
|
||||
case 'Serif':
|
||||
return t['com.affine.appearanceSettings.fontStyle.serif']();
|
||||
case 'Mono':
|
||||
return t[`com.affine.appearanceSettings.fontStyle.mono`]();
|
||||
case 'Custom':
|
||||
return t['com.affine.settings.editorSettings.edgeless.custom']();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getBaseFontStyleOptions = (
|
||||
t: ReturnType<typeof useI18n>
|
||||
): Array<Omit<RadioItem, 'value'> & { value: FontFamily }> => {
|
||||
return fontStyleOptions
|
||||
.map(({ key, value }) => {
|
||||
if (key === 'Custom') {
|
||||
return null;
|
||||
}
|
||||
const label = getLabel(key, t);
|
||||
return {
|
||||
value: key,
|
||||
label,
|
||||
testId: 'system-font-style-trigger',
|
||||
style: {
|
||||
fontFamily: value,
|
||||
},
|
||||
} satisfies RadioItem;
|
||||
})
|
||||
.filter(item => item !== null);
|
||||
};
|
||||
|
||||
const FontFamilySettings = () => {
|
||||
const t = useI18n();
|
||||
const { editorSettingService } = useServices({ EditorSettingService });
|
||||
const settings = useLiveData(editorSettingService.editorSetting.settings$);
|
||||
|
||||
const radioItems = useMemo(() => {
|
||||
const items = getBaseFontStyleOptions(t);
|
||||
if (!BUILD_CONFIG.isElectron) return items;
|
||||
|
||||
// resolve custom fonts
|
||||
const customOption = fontStyleOptions.find(opt => opt.key === 'Custom');
|
||||
if (customOption) {
|
||||
const fontFamily = settings.customFontFamily
|
||||
? `${settings.customFontFamily}, ${customOption.value}`
|
||||
: customOption.value;
|
||||
items.push({
|
||||
value: customOption.key,
|
||||
label: getLabel(customOption.key, t),
|
||||
testId: 'system-font-style-trigger',
|
||||
style: { fontFamily },
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [settings.customFontFamily, t]);
|
||||
|
||||
const handleFontFamilyChange = useCallback(
|
||||
(value: FontFamily) => {
|
||||
editorSettingService.editorSetting.set('fontFamily', value);
|
||||
},
|
||||
[editorSettingService.editorSetting]
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.font.title']()}
|
||||
desc={t['com.affine.appearanceSettings.font.description']()}
|
||||
>
|
||||
<RadioGroup
|
||||
items={radioItems}
|
||||
value={settings.fontFamily}
|
||||
width={250}
|
||||
className={styles.settingWrapper}
|
||||
onChange={handleFontFamilyChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
const getFontFamily = (font: string) => `${font}, ${fontStyleOptions[0].value}`;
|
||||
|
||||
const Scroller = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<HTMLAttributes<HTMLDivElement>>
|
||||
>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport {...props} ref={ref}>
|
||||
{children}
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
);
|
||||
});
|
||||
|
||||
Scroller.displayName = 'Scroller';
|
||||
|
||||
const FontMenuItems = ({ onSelect }: { onSelect: (font: string) => void }) => {
|
||||
const { systemFontFamilyService, editorSettingService } = useServices({
|
||||
SystemFontFamilyService,
|
||||
EditorSettingService,
|
||||
});
|
||||
const systemFontFamily = systemFontFamilyService.systemFontFamily;
|
||||
|
||||
const currentCustomFont = useLiveData(
|
||||
editorSettingService.editorSetting.settings$
|
||||
).customFontFamily;
|
||||
useEffect(() => {
|
||||
if (systemFontFamily.fontList$.value.length === 0) {
|
||||
systemFontFamily.loadFontList();
|
||||
}
|
||||
systemFontFamily.clearSearch();
|
||||
}, [systemFontFamily]);
|
||||
|
||||
const isLoading = useLiveData(systemFontFamily.isLoading$);
|
||||
const result = useLiveData(systemFontFamily.result$);
|
||||
const searchText = useLiveData(systemFontFamily.searchText$);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(value: string) => {
|
||||
systemFontFamily.search(value);
|
||||
},
|
||||
[systemFontFamily]
|
||||
);
|
||||
const onInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation(); // avoid typeahead search built-in in the menu
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.InputContainer}>
|
||||
<SearchIcon className={styles.searchIcon} />
|
||||
<RowInput
|
||||
value={searchText ?? ''}
|
||||
onChange={onInputChange}
|
||||
onKeyDown={onInputKeyDown}
|
||||
autoFocus
|
||||
className={styles.searchInput}
|
||||
placeholder="Fonts"
|
||||
/>
|
||||
</div>
|
||||
<MenuSeparator />
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Scrollable.Root style={{ height: '330px' }}>
|
||||
<Scrollable.Viewport>
|
||||
{result.length > 0 ? (
|
||||
<Virtuoso
|
||||
totalCount={result.length}
|
||||
components={{
|
||||
Scroller: Scroller,
|
||||
}}
|
||||
itemContent={index => (
|
||||
<FontMenuItem
|
||||
key={result[index].fullName}
|
||||
font={result[index]}
|
||||
onSelect={onSelect}
|
||||
currentFont={currentCustomFont}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.notFound}>No results found.</div>
|
||||
)}
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FontMenuItem = ({
|
||||
font,
|
||||
currentFont,
|
||||
onSelect,
|
||||
}: {
|
||||
font: FontData;
|
||||
currentFont: string;
|
||||
onSelect: (font: string) => void;
|
||||
}) => {
|
||||
const handleFontSelect = useCallback(
|
||||
() => onSelect(font.family),
|
||||
[font, onSelect]
|
||||
);
|
||||
const fontFamily = getFontFamily(font.family);
|
||||
const selected = currentFont === font.fullName;
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<MenuItem key={font.fullName} onSelect={handleFontSelect}>
|
||||
<div className={styles.fontItemContainer}>
|
||||
<div className={styles.fontItem}>
|
||||
<div className={styles.fontLabel} style={{ fontFamily }}>
|
||||
{font.fullName}
|
||||
</div>
|
||||
<div className={clsx(styles.fontLabel, 'secondary')}>
|
||||
{font.fullName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<DoneIcon fontSize={20} className={styles.selectedIcon} />
|
||||
)}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomFontFamilySettings = () => {
|
||||
const t = useI18n();
|
||||
const { editorSettingService } = useServices({ EditorSettingService });
|
||||
const settings = useLiveData(editorSettingService.editorSetting.settings$);
|
||||
|
||||
const fontFamily = getFontFamily(settings.customFontFamily);
|
||||
|
||||
const onCustomFontFamilyChange = useCallback(
|
||||
(fontFamily: string) => {
|
||||
editorSettingService.editorSetting.set('customFontFamily', fontFamily);
|
||||
},
|
||||
[editorSettingService.editorSetting]
|
||||
);
|
||||
if (settings.fontFamily !== 'Custom' || !BUILD_CONFIG.isElectron) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.general.font-family.custom.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.general.font-family.custom.description'
|
||||
]()}
|
||||
>
|
||||
<Menu
|
||||
items={<FontMenuItems onSelect={onCustomFontFamilyChange} />}
|
||||
contentOptions={{
|
||||
align: 'end',
|
||||
style: { width: '250px', height: '380px' },
|
||||
}}
|
||||
>
|
||||
<MenuTrigger className={styles.menuTrigger} style={{ fontFamily }}>
|
||||
{settings.customFontFamily || 'Select a font'}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
const NewDocDefaultModeSettings = () => {
|
||||
const t = useI18n();
|
||||
const { editorSettingService } = useServices({ EditorSettingService });
|
||||
const settings = useLiveData(editorSettingService.editorSetting.settings$);
|
||||
const radioItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: 'page',
|
||||
label: t['Page'](),
|
||||
testId: 'page-mode-trigger',
|
||||
},
|
||||
{
|
||||
value: 'edgeless',
|
||||
label: t['Edgeless'](),
|
||||
testId: 'edgeless-mode-trigger',
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
const updateNewDocDefaultMode = useCallback(
|
||||
(value: DocMode) => {
|
||||
editorSettingService.editorSetting.set('newDocDefaultMode', value);
|
||||
},
|
||||
[editorSettingService.editorSetting]
|
||||
);
|
||||
return (
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.general.default-new-doc.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.general.default-new-doc.description'
|
||||
]()}
|
||||
>
|
||||
<RadioGroup
|
||||
items={radioItems}
|
||||
value={settings.newDocDefaultMode}
|
||||
width={250}
|
||||
className={styles.settingWrapper}
|
||||
onChange={updateNewDocDefaultMode}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const DeFaultCodeBlockSettings = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.general.default-code-block.language.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.general.default-code-block.language.description'
|
||||
]()}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={<MenuItem>Plain Text</MenuItem>}
|
||||
trigger={
|
||||
<MenuTrigger className={styles.menuTrigger} disabled>
|
||||
Plain Text
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.general.default-code-block.wrap.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.general.default-code-block.wrap.description'
|
||||
]()}
|
||||
>
|
||||
<Switch />
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SpellCheckSettings = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.general.spell-check.title']()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.general.spell-check.description'
|
||||
]()}
|
||||
>
|
||||
<Switch />
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
const AISettings = () => {
|
||||
const t = useI18n();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { featureFlagService, serverConfigService } = useServices({
|
||||
FeatureFlagService,
|
||||
ServerConfigService,
|
||||
});
|
||||
const serverFeatures = useLiveData(
|
||||
serverConfigService.serverConfig.features$
|
||||
);
|
||||
const enableAI = useLiveData(featureFlagService.flags.enable_ai.$);
|
||||
|
||||
const onAIChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
featureFlagService.flags.enable_ai.set(checked); // this will trigger page reload, see `FeatureFlagService`
|
||||
},
|
||||
[featureFlagService]
|
||||
);
|
||||
const onToggleAI = useCallback(
|
||||
(checked: boolean) => {
|
||||
openConfirmModal({
|
||||
title: checked
|
||||
? t['com.affine.settings.editorSettings.general.ai.enable.title']()
|
||||
: t['com.affine.settings.editorSettings.general.ai.disable.title'](),
|
||||
description: checked
|
||||
? t[
|
||||
'com.affine.settings.editorSettings.general.ai.enable.description'
|
||||
]()
|
||||
: t[
|
||||
'com.affine.settings.editorSettings.general.ai.disable.description'
|
||||
](),
|
||||
confirmText: checked
|
||||
? t['com.affine.settings.editorSettings.general.ai.enable.confirm']()
|
||||
: t[
|
||||
'com.affine.settings.editorSettings.general.ai.disable.confirm'
|
||||
](),
|
||||
cancelText: t['Cancel'](),
|
||||
onConfirm: () => onAIChange(checked),
|
||||
confirmButtonOptions: {
|
||||
variant: checked ? 'primary' : 'error',
|
||||
},
|
||||
});
|
||||
},
|
||||
[openConfirmModal, t, onAIChange]
|
||||
);
|
||||
|
||||
if (!serverFeatures?.copilot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.general.ai.title']()}
|
||||
desc={t['com.affine.settings.editorSettings.general.ai.description']()}
|
||||
>
|
||||
<Switch checked={enableAI} onChange={onToggleAI} />
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const General = () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<SettingWrapper title={t['com.affine.settings.editorSettings.general']()}>
|
||||
<AISettings />
|
||||
<FontFamilySettings />
|
||||
<CustomFontFamilySettings />
|
||||
<NewDocDefaultModeSettings />
|
||||
{/* // TODO(@akumatus): implement these settings
|
||||
<DeFaultCodeBlockSettings />
|
||||
<SpellCheckSettings /> */}
|
||||
</SettingWrapper>
|
||||
);
|
||||
};
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
|
||||
import { Edgeless } from './edgeless';
|
||||
import { General } from './general';
|
||||
import { Page } from './page';
|
||||
|
||||
export const EditorSettings = () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['com.affine.settings.editorSettings.title']()}
|
||||
subtitle={t['com.affine.settings.editorSettings.subtitle']()}
|
||||
/>
|
||||
<General />
|
||||
<Page />
|
||||
<Edgeless />
|
||||
|
||||
{/* // TODO(@EYHN): implement export and import
|
||||
<Preferences /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { Menu } from '@affine/component';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export const DropdownMenu = ({
|
||||
items,
|
||||
trigger,
|
||||
}: {
|
||||
items: ReactNode;
|
||||
trigger: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Menu
|
||||
items={items}
|
||||
contentOptions={{
|
||||
style: {
|
||||
width: '250px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{trigger}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import { Switch } from '@affine/component';
|
||||
import {
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const Page = () => {
|
||||
const t = useI18n();
|
||||
const editorSetting = useService(EditorSettingService).editorSetting;
|
||||
const settings = useLiveData(editorSetting.settings$);
|
||||
|
||||
const handleFullWidthLayoutChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
editorSetting.set('fullWidthLayout', checked);
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
const handleDisplayDocInfoChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
editorSetting.set('displayDocInfo', checked);
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
const handleDisplayBiDirectionalLinkChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
editorSetting.set('displayBiDirectionalLink', checked);
|
||||
},
|
||||
[editorSetting]
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingWrapper title={t['com.affine.settings.editorSettings.page']()}>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.editorSettings.page.full-width.title']()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.page.full-width.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
data-testid="full-width-layout-trigger"
|
||||
checked={settings.fullWidthLayout}
|
||||
onChange={handleFullWidthLayoutChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.page.display-doc-info.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.page.display-doc-info.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
data-testid="display-doc-info-trigger"
|
||||
checked={settings.displayDocInfo}
|
||||
onChange={handleDisplayDocInfoChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.page.display-bi-link.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.page.display-bi-link.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
data-testid="display-bi-link-trigger"
|
||||
checked={settings.displayBiDirectionalLink}
|
||||
onChange={handleDisplayBiDirectionalLinkChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
);
|
||||
};
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { Button } from '@affine/component';
|
||||
import {
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
|
||||
export const Preferences = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<SettingWrapper
|
||||
title={t['com.affine.settings.editorSettings.preferences']()}
|
||||
>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.preferences.export.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.preferences.export.description'
|
||||
]()}
|
||||
>
|
||||
<Button>Export</Button>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.settings.editorSettings.preferences.import.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.settings.editorSettings.preferences.import.description'
|
||||
]()}
|
||||
>
|
||||
<Button>Import</Button>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
);
|
||||
};
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const settingWrapper = style({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
minWidth: '150px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
export const preViewLabelWrapper = style({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
minWidth: '100px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
|
||||
export const menu = style({
|
||||
background: 'white',
|
||||
width: '250px',
|
||||
maxHeight: '30vh',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
|
||||
export const menuTrigger = style({
|
||||
textTransform: 'capitalize',
|
||||
fontWeight: 600,
|
||||
width: '250px',
|
||||
});
|
||||
|
||||
export const snapshotContainer = style({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginBottom: '24px',
|
||||
});
|
||||
|
||||
export const snapshotTitle = style({
|
||||
marginBottom: '8px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
|
||||
export const snapshotSkeleton = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
});
|
||||
|
||||
export const snapshot = style({
|
||||
width: '100%',
|
||||
height: '180px',
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const snapshotLabel = style({
|
||||
position: 'absolute',
|
||||
bottom: '12px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVarV2('text/secondary'),
|
||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||
padding: '2px 8px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '4px',
|
||||
zIndex: 1,
|
||||
height: '24px',
|
||||
});
|
||||
|
||||
export const shapeIndicator = style({
|
||||
boxShadow: 'none',
|
||||
backgroundColor: cssVarV2('layer/background/tertiary'),
|
||||
});
|
||||
export const InputContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-start',
|
||||
gap: '6px',
|
||||
});
|
||||
export const searchInput = style({
|
||||
flexGrow: 1,
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontFamily: 'inherit',
|
||||
color: 'inherit',
|
||||
backgroundColor: 'transparent',
|
||||
'::placeholder': {
|
||||
color: cssVarV2('text/placeholder'),
|
||||
},
|
||||
});
|
||||
export const searchIcon = style({
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: '20px',
|
||||
});
|
||||
export const fontItemContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
});
|
||||
export const fontItem = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
});
|
||||
export const fontLabel = style({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
color: cssVarV2('text/primary'),
|
||||
width: '100%',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: 'normal',
|
||||
height: '22px',
|
||||
selectors: {
|
||||
'&.secondary': {
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
height: '20px',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const selectedIcon = style({
|
||||
color: cssVarV2('button/primary'),
|
||||
marginLeft: '8px',
|
||||
});
|
||||
export const notFound = style({
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
padding: '4px',
|
||||
});
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import { FontWeight } from '@blocksuite/affine/blocks';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
function getColorFromMap(
|
||||
color: string | { normal: string } | { light: string; dark: string },
|
||||
colorMap: { [key: string]: string },
|
||||
theme: 'light' | 'dark' = 'light'
|
||||
):
|
||||
| {
|
||||
value: string;
|
||||
key: string;
|
||||
}
|
||||
| undefined {
|
||||
if (typeof color === 'string') {
|
||||
return { value: color, key: colorMap[color] };
|
||||
}
|
||||
|
||||
if ('normal' in color) {
|
||||
return {
|
||||
value: color.normal,
|
||||
key: colorMap[color.normal],
|
||||
};
|
||||
}
|
||||
|
||||
if ('light' in color && 'dark' in color) {
|
||||
return {
|
||||
value: color[theme],
|
||||
key: colorMap[color[theme]],
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const useColor = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
return (
|
||||
color: string | { normal: string } | { light: string; dark: string },
|
||||
colorMap: { [key: string]: string }
|
||||
) =>
|
||||
getColorFromMap(
|
||||
color,
|
||||
colorMap,
|
||||
resolvedTheme as 'light' | 'dark' | undefined
|
||||
);
|
||||
};
|
||||
|
||||
export const sortedFontWeightEntries = Object.entries(FontWeight).sort(
|
||||
(a, b) => Number(a[1]) - Number(b[1])
|
||||
);
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
height: 100,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import * as styles from './arts.css';
|
||||
import DarkSvg from './dark-art-svg';
|
||||
import LightSvg from './light-art-svg';
|
||||
|
||||
export const ExperimentalFeatureArts = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: resolvedTheme === 'dark' ? DarkSvg : LightSvg,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-857
@@ -1,857 +0,0 @@
|
||||
export default `<svg
|
||||
width="429"
|
||||
height="101"
|
||||
viewBox="0 0 429 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g opacity="0.3" filter="url(#filter0_i_909_33931)">
|
||||
<g clip-path="url(#clip0_909_33931)">
|
||||
<rect
|
||||
x="0.505859"
|
||||
y="30.9414"
|
||||
width="40"
|
||||
height="40"
|
||||
rx="6.06061"
|
||||
fill="#454545"
|
||||
fill-opacity="0.42"
|
||||
/>
|
||||
<g filter="url(#filter1_d_909_33931)">
|
||||
<rect
|
||||
x="2.32397"
|
||||
y="32.7595"
|
||||
width="36.3636"
|
||||
height="36.3636"
|
||||
rx="3.96118"
|
||||
fill="url(#paint0_linear_909_33931)"
|
||||
/>
|
||||
<rect
|
||||
x="2.47549"
|
||||
y="32.911"
|
||||
width="36.0606"
|
||||
height="36.0606"
|
||||
rx="3.80966"
|
||||
stroke="#757575"
|
||||
stroke-width="0.30303"
|
||||
/>
|
||||
<g filter="url(#filter2_i_909_33931)">
|
||||
<path
|
||||
d="M29.6836 40.9414H20.0521C20.0521 43.3415 21.998 45.2874 24.398 45.2874H26.1719V47.0019C26.1739 49.3966 28.1125 51.3379 30.5052 51.3445H30.5059V41.7797C30.5059 41.7783 30.5059 41.7777 30.5059 41.7763C30.5059 41.3195 30.1391 40.9487 29.6843 40.9421H29.6836V40.9414ZM24.9182 45.7388H15.2866C15.2873 48.1389 17.2325 50.0841 19.6326 50.0841H21.4065V51.7993C21.4085 54.1987 23.353 56.1426 25.7524 56.1446V46.5738C25.7524 46.113 25.379 45.7395 24.9182 45.7395V45.7388ZM20.1488 50.5356H10.5059C10.5072 52.9363 12.4538 54.8816 14.8545 54.8816C14.8585 54.8816 14.8625 54.8816 14.8665 54.8816H16.641V56.5954C16.641 58.9948 18.5856 60.9401 20.985 60.9414V51.3732C20.985 50.9104 20.6102 50.5356 20.1474 50.5356H20.1488Z"
|
||||
fill="#D0CFCF"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g opacity="0.5" filter="url(#filter3_i_909_33931)">
|
||||
<g clip-path="url(#clip1_909_33931)">
|
||||
<rect
|
||||
x="72.5059"
|
||||
y="20.9414"
|
||||
width="60"
|
||||
height="60"
|
||||
rx="9.09091"
|
||||
fill="#454545"
|
||||
fill-opacity="0.42"
|
||||
/>
|
||||
<g filter="url(#filter4_d_909_33931)">
|
||||
<rect
|
||||
x="75.2332"
|
||||
y="23.6687"
|
||||
width="54.5455"
|
||||
height="54.5455"
|
||||
rx="5.94177"
|
||||
fill="url(#paint1_linear_909_33931)"
|
||||
/>
|
||||
<rect
|
||||
x="75.4604"
|
||||
y="23.896"
|
||||
width="54.0909"
|
||||
height="54.0909"
|
||||
rx="5.7145"
|
||||
stroke="#757575"
|
||||
stroke-width="0.454545"
|
||||
/>
|
||||
<g filter="url(#filter5_i_909_33931)">
|
||||
<path
|
||||
d="M107.701 47.2596H101.514V35.1914H107.701C111.04 35.1914 113.756 37.8977 113.756 41.2248C113.756 44.552 111.04 47.2596 107.701 47.2596ZM103.498 45.283H107.701C109.947 45.283 111.772 43.4622 111.772 41.2262C111.772 38.9902 109.945 37.1694 107.701 37.1694H103.498V45.283ZM103.498 47.2596H97.3123C93.9734 47.2596 91.2575 44.5533 91.2575 41.2262C91.2575 37.8991 93.9734 35.1914 97.3123 35.1914H103.499V47.2596H103.498ZM97.3123 37.1681C95.067 37.1681 93.2411 38.9888 93.2411 41.2248C93.2411 43.4608 95.067 45.283 97.3123 45.283H101.516V37.1681H97.3123ZM103.498 57.3498H97.3123C93.9734 57.3498 91.2575 54.6435 91.2575 51.3164C91.2575 47.9893 93.9734 45.283 97.3123 45.283H103.499V57.3498H103.498ZM97.3123 47.2596C95.067 47.2596 93.2411 49.0804 93.2411 51.3164C93.2411 53.5524 95.0684 55.3732 97.3123 55.3732H101.516V47.2596H97.3123ZM97.3447 67.4414C93.9882 67.4414 91.2561 64.7351 91.2561 61.408C91.2561 58.0808 93.972 55.3745 97.3109 55.3745H103.498V61.3421C103.498 64.7055 100.738 67.4414 97.3447 67.4414ZM97.3123 57.3498C96.233 57.3513 95.1983 57.7791 94.4351 58.5396C93.6719 59.3001 93.2425 60.3311 93.2411 61.4066C93.2411 63.644 95.0818 65.4634 97.346 65.4634C99.6452 65.4634 101.517 63.6144 101.517 61.3408V57.3498H97.3123ZM107.701 57.3498H107.569C104.23 57.3498 101.514 54.6435 101.514 51.3164C101.514 47.9893 104.23 45.283 107.569 45.283H107.701C111.04 45.283 113.756 47.9893 113.756 51.3164C113.756 54.6435 111.04 57.3498 107.701 57.3498ZM107.57 47.2596C105.325 47.2596 103.499 49.0804 103.499 51.3164C103.499 53.5524 105.327 55.3732 107.57 55.3732H107.703C109.948 55.3732 111.774 53.5524 111.774 51.3164C111.774 49.0804 109.945 47.2596 107.701 47.2596H107.57Z"
|
||||
fill="#3E3E3E"
|
||||
/>
|
||||
<path
|
||||
d="M107.701 47.2596H101.514V35.1914H107.701C111.04 35.1914 113.756 37.8977 113.756 41.2248C113.756 44.552 111.04 47.2596 107.701 47.2596ZM103.498 45.283H107.701C109.947 45.283 111.772 43.4622 111.772 41.2262C111.772 38.9902 109.945 37.1694 107.701 37.1694H103.498V45.283ZM103.498 47.2596H97.3123C93.9734 47.2596 91.2575 44.5533 91.2575 41.2262C91.2575 37.8991 93.9734 35.1914 97.3123 35.1914H103.499V47.2596H103.498ZM97.3123 37.1681C95.067 37.1681 93.2411 38.9888 93.2411 41.2248C93.2411 43.4608 95.067 45.283 97.3123 45.283H101.516V37.1681H97.3123ZM103.498 57.3498H97.3123C93.9734 57.3498 91.2575 54.6435 91.2575 51.3164C91.2575 47.9893 93.9734 45.283 97.3123 45.283H103.499V57.3498H103.498ZM97.3123 47.2596C95.067 47.2596 93.2411 49.0804 93.2411 51.3164C93.2411 53.5524 95.0684 55.3732 97.3123 55.3732H101.516V47.2596H97.3123ZM97.3447 67.4414C93.9882 67.4414 91.2561 64.7351 91.2561 61.408C91.2561 58.0808 93.972 55.3745 97.3109 55.3745H103.498V61.3421C103.498 64.7055 100.738 67.4414 97.3447 67.4414ZM97.3123 57.3498C96.233 57.3513 95.1983 57.7791 94.4351 58.5396C93.6719 59.3001 93.2425 60.3311 93.2411 61.4066C93.2411 63.644 95.0818 65.4634 97.346 65.4634C99.6452 65.4634 101.517 63.6144 101.517 61.3408V57.3498H97.3123ZM107.701 57.3498H107.569C104.23 57.3498 101.514 54.6435 101.514 51.3164C101.514 47.9893 104.23 45.283 107.569 45.283H107.701C111.04 45.283 113.756 47.9893 113.756 51.3164C113.756 54.6435 111.04 57.3498 107.701 57.3498ZM107.57 47.2596C105.325 47.2596 103.499 49.0804 103.499 51.3164C103.499 53.5524 105.327 55.3732 107.57 55.3732H107.703C109.948 55.3732 111.774 53.5524 111.774 51.3164C111.774 49.0804 109.945 47.2596 107.701 47.2596H107.57Z"
|
||||
fill="#D0CFCF"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g filter="url(#filter6_i_909_33931)">
|
||||
<g clip-path="url(#clip2_909_33931)">
|
||||
<rect
|
||||
x="164.506"
|
||||
y="0.941406"
|
||||
width="100"
|
||||
height="100"
|
||||
rx="15.1515"
|
||||
fill="#454545"
|
||||
fill-opacity="0.42"
|
||||
/>
|
||||
<g filter="url(#filter7_d_909_33931)">
|
||||
<rect
|
||||
x="169.051"
|
||||
y="5.48682"
|
||||
width="90.9091"
|
||||
height="90.9091"
|
||||
rx="9.90295"
|
||||
fill="url(#paint2_linear_909_33931)"
|
||||
/>
|
||||
<rect
|
||||
x="169.43"
|
||||
y="5.8656"
|
||||
width="90.1515"
|
||||
height="90.1515"
|
||||
rx="9.52416"
|
||||
stroke="#595959"
|
||||
stroke-width="0.757576"
|
||||
/>
|
||||
<g filter="url(#filter8_i_909_33931)">
|
||||
<path
|
||||
d="M194.424 41.464C194.424 39.6297 195.152 37.8706 196.449 36.5736C197.746 35.2766 199.506 34.548 201.34 34.548H207.917C208.602 34.548 209.023 33.8356 208.761 33.204C208.628 32.8934 208.523 32.5717 208.447 32.2426C207.684 28.8146 210.273 25.3267 214.019 25.3267C217.768 25.3267 220.354 28.8146 219.591 32.2426C219.515 32.5717 219.41 32.8934 219.278 33.204C219.012 33.8356 219.437 34.548 220.121 34.548H224.393C226.227 34.548 227.986 35.2766 229.283 36.5736C230.58 37.8706 231.309 39.6297 231.309 41.464V45.7357C231.309 46.4204 232.019 46.8423 232.653 46.5795C232.964 46.4504 233.282 46.3397 233.614 46.266C237.042 45.5029 240.53 48.0895 240.53 51.8379C240.53 55.5864 237.042 58.173 233.614 57.4099C233.285 57.3341 232.964 57.2291 232.653 57.0964C232.021 56.8313 231.309 57.2554 231.309 57.9401V64.5172C231.309 66.3515 230.58 68.1106 229.283 69.4076C227.986 70.7046 226.227 71.4332 224.393 71.4332H220.029C219.365 71.4332 218.939 70.7624 219.13 70.1284C219.225 69.8057 219.296 69.4737 219.326 69.1279C219.387 68.3938 219.296 67.655 219.057 66.9581C218.818 66.2613 218.437 65.6216 217.938 65.0795C217.439 64.5373 216.834 64.1046 216.159 63.8086C215.484 63.5126 214.756 63.3598 214.019 63.3598C213.282 63.3598 212.554 63.5126 211.879 63.8086C211.205 64.1046 210.599 64.5373 210.1 65.0795C209.601 65.6216 209.22 66.2613 208.982 66.9581C208.743 67.655 208.651 68.3938 208.712 69.1279C208.742 69.4737 208.814 69.8057 208.911 70.1284C209.1 70.7624 208.673 71.4332 208.011 71.4332H201.34C199.506 71.4332 197.746 70.7046 196.449 69.4076C195.152 68.1106 194.424 66.3515 194.424 64.5172V57.8479C194.424 57.184 195.095 56.7575 195.729 56.9488C196.051 57.0434 196.383 57.1148 196.729 57.1448C197.463 57.206 198.202 57.1143 198.899 56.8754C199.596 56.6366 200.235 56.2557 200.778 55.7569C201.32 55.2582 201.752 54.6523 202.048 53.9778C202.344 53.3032 202.497 52.5746 202.497 51.8379C202.497 51.1013 202.344 50.3726 202.048 49.6981C201.752 49.0235 201.32 48.4177 200.778 47.9189C200.235 47.4202 199.596 47.0393 198.899 46.8004C198.202 46.5615 197.463 46.4698 196.729 46.5311C196.383 46.561 196.051 46.6325 195.729 46.7293C195.095 46.9184 194.424 46.4919 194.424 45.8302V41.464Z"
|
||||
fill="#D0CFCF"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g opacity="0.5" filter="url(#filter9_i_909_33931)">
|
||||
<g clip-path="url(#clip3_909_33931)">
|
||||
<rect
|
||||
x="296.506"
|
||||
y="20.9414"
|
||||
width="60"
|
||||
height="60"
|
||||
rx="9.09091"
|
||||
fill="#454545"
|
||||
fill-opacity="0.42"
|
||||
/>
|
||||
<g filter="url(#filter10_d_909_33931)">
|
||||
<rect
|
||||
x="299.233"
|
||||
y="23.6687"
|
||||
width="54.5455"
|
||||
height="54.5455"
|
||||
rx="5.94177"
|
||||
fill="url(#paint3_linear_909_33931)"
|
||||
/>
|
||||
<rect
|
||||
x="299.46"
|
||||
y="23.896"
|
||||
width="54.0909"
|
||||
height="54.0909"
|
||||
rx="5.7145"
|
||||
stroke="#757575"
|
||||
stroke-width="0.454545"
|
||||
/>
|
||||
<g
|
||||
clip-path="url(#clip4_909_33931)"
|
||||
filter="url(#filter11_i_909_33931)"
|
||||
>
|
||||
<path
|
||||
d="M326.539 32.9546C316.589 32.9546 308.519 41.023 308.519 50.9746C308.519 58.9364 313.682 65.6909 320.843 68.074C321.743 68.2407 322.034 67.6821 322.034 67.2076V63.8528C317.022 64.943 315.978 61.7265 315.978 61.7265C315.158 59.6437 313.976 59.0896 313.976 59.0896C312.341 57.9708 314.101 57.9948 314.101 57.9948C315.91 58.121 316.863 59.8524 316.863 59.8524C318.469 62.6065 321.078 61.8106 322.106 61.3496C322.267 60.1858 322.734 59.3899 323.251 58.9409C319.249 58.4829 315.041 56.9377 315.041 50.0345C315.041 48.0658 315.745 46.4591 316.897 45.1977C316.711 44.7427 316.094 42.9091 317.073 40.4284C317.073 40.4284 318.586 39.9448 322.03 42.2754C323.467 41.876 325.008 41.6763 326.539 41.6687C328.071 41.6763 329.613 41.876 331.053 42.2754C334.494 39.9448 336.004 40.4284 336.004 40.4284C336.985 42.9106 336.368 44.7442 336.181 45.1977C337.338 46.4591 338.036 48.0673 338.036 50.0345C338.036 56.9557 333.821 58.4799 329.808 58.9259C330.454 59.4845 331.044 60.5807 331.044 62.2626V67.2076C331.044 67.6866 331.333 68.2497 332.247 68.0725C339.403 65.6864 344.559 58.9334 344.559 50.9746C344.559 41.023 336.491 32.9546 326.539 32.9546Z"
|
||||
fill="#D0CFCF"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g opacity="0.3" filter="url(#filter12_i_909_33931)">
|
||||
<g clip-path="url(#clip5_909_33931)">
|
||||
<rect
|
||||
x="388.506"
|
||||
y="30.9414"
|
||||
width="40"
|
||||
height="40"
|
||||
rx="6.06061"
|
||||
fill="#454545"
|
||||
fill-opacity="0.42"
|
||||
/>
|
||||
<g filter="url(#filter13_d_909_33931)">
|
||||
<rect
|
||||
x="390.324"
|
||||
y="32.7595"
|
||||
width="36.3636"
|
||||
height="36.3636"
|
||||
rx="3.96118"
|
||||
fill="url(#paint4_linear_909_33931)"
|
||||
/>
|
||||
<rect
|
||||
x="390.475"
|
||||
y="32.911"
|
||||
width="36.0606"
|
||||
height="36.0606"
|
||||
rx="3.80966"
|
||||
stroke="#757575"
|
||||
stroke-width="0.30303"
|
||||
/>
|
||||
<g filter="url(#filter14_i_909_33931)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M414.856 44.5774C411.355 41.0764 405.687 41.0628 402.169 44.5366L414.897 57.2642C418.37 53.7463 418.357 48.0785 414.856 44.5774ZM400.373 47.0514L412.382 59.06C413.005 58.7615 413.6 58.3872 414.155 57.937L401.496 45.278C401.046 45.8326 400.672 46.4284 400.373 47.0514ZM408.998 59.9272L399.506 50.4352C399.551 49.6297 399.704 48.8291 399.964 48.0569L411.376 59.4687C410.604 59.7293 409.803 59.8821 408.998 59.9272ZM402.128 57.3054C400.611 55.7882 399.748 53.8641 399.541 51.8845L407.549 59.8922C405.569 59.6848 403.645 58.8225 402.128 57.3054Z"
|
||||
fill="#D0CFCF"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_i_909_33931"
|
||||
x="0.505859"
|
||||
y="30.7394"
|
||||
width="40"
|
||||
height="40.202"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.20202" />
|
||||
<feGaussianBlur stdDeviation="0.707071" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_d_909_33931"
|
||||
x="0.808823"
|
||||
y="31.7494"
|
||||
width="39.3938"
|
||||
height="39.3938"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.505051" />
|
||||
<feGaussianBlur stdDeviation="0.757576" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_909_33931"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_909_33931"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_i_909_33931"
|
||||
x="8.51465"
|
||||
y="38.9502"
|
||||
width="24.2725"
|
||||
height="24.2725"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.245902" dy="0.245902" />
|
||||
<feGaussianBlur stdDeviation="0.327869" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter3_i_909_33931"
|
||||
x="72.5059"
|
||||
y="20.6384"
|
||||
width="60"
|
||||
height="60.303"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.30303" />
|
||||
<feGaussianBlur stdDeviation="1.06061" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter4_d_909_33931"
|
||||
x="72.9604"
|
||||
y="22.1535"
|
||||
width="59.0909"
|
||||
height="59.0909"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.757576" />
|
||||
<feGaussianBlur stdDeviation="1.13636" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_909_33931"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_909_33931"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter5_i_909_33931"
|
||||
x="84.5193"
|
||||
y="32.9546"
|
||||
width="36.4089"
|
||||
height="36.4089"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.368852" dy="0.368852" />
|
||||
<feGaussianBlur stdDeviation="0.491803" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter6_i_909_33931"
|
||||
x="164.506"
|
||||
y="0.436356"
|
||||
width="100"
|
||||
height="100.505"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.50505" />
|
||||
<feGaussianBlur stdDeviation="1.76768" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter7_d_909_33931"
|
||||
x="165.263"
|
||||
y="2.96156"
|
||||
width="98.4849"
|
||||
height="98.4849"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1.26263" />
|
||||
<feGaussianBlur stdDeviation="1.89394" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_909_33931"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_909_33931"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter8_i_909_33931"
|
||||
x="194.424"
|
||||
y="25.3267"
|
||||
width="46.7212"
|
||||
height="46.7212"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.614754" dy="0.614754" />
|
||||
<feGaussianBlur stdDeviation="0.819672" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter9_i_909_33931"
|
||||
x="296.506"
|
||||
y="20.6384"
|
||||
width="60"
|
||||
height="60.303"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.30303" />
|
||||
<feGaussianBlur stdDeviation="1.06061" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter10_d_909_33931"
|
||||
x="296.96"
|
||||
y="22.1535"
|
||||
width="59.0909"
|
||||
height="59.0909"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.757576" />
|
||||
<feGaussianBlur stdDeviation="1.13636" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_909_33931"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_909_33931"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter11_i_909_33931"
|
||||
x="308.519"
|
||||
y="32.9546"
|
||||
width="36.4089"
|
||||
height="36.4089"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.368852" dy="0.368852" />
|
||||
<feGaussianBlur stdDeviation="0.491803" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter12_i_909_33931"
|
||||
x="388.506"
|
||||
y="30.7394"
|
||||
width="40"
|
||||
height="40.202"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.20202" />
|
||||
<feGaussianBlur stdDeviation="0.707071" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter13_d_909_33931"
|
||||
x="388.809"
|
||||
y="31.7494"
|
||||
width="39.3938"
|
||||
height="39.3938"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.505051" />
|
||||
<feGaussianBlur stdDeviation="0.757576" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_909_33931"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_909_33931"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter14_i_909_33931"
|
||||
x="396.515"
|
||||
y="38.9502"
|
||||
width="24.2725"
|
||||
height="24.2725"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.245902" dy="0.245902" />
|
||||
<feGaussianBlur stdDeviation="0.327869" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_909_33931"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_909_33931"
|
||||
x1="7.37448"
|
||||
y1="33.5171"
|
||||
x2="28.6876"
|
||||
y2="69.5777"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#383838" />
|
||||
<stop offset="0.260417" stop-color="#2A2A2A" />
|
||||
<stop offset="0.619792" stop-color="#393939" />
|
||||
<stop offset="1" stop-color="#414141" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_909_33931"
|
||||
x1="82.8089"
|
||||
y1="24.8051"
|
||||
x2="114.779"
|
||||
y2="78.896"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#383838" />
|
||||
<stop offset="0.260417" stop-color="#2A2A2A" />
|
||||
<stop offset="0.619792" stop-color="#393939" />
|
||||
<stop offset="1" stop-color="#414141" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_909_33931"
|
||||
x1="181.678"
|
||||
y1="7.38076"
|
||||
x2="234.96"
|
||||
y2="97.5323"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#383838" />
|
||||
<stop offset="0.260417" stop-color="#2A2A2A" />
|
||||
<stop offset="0.619792" stop-color="#393939" />
|
||||
<stop offset="1" stop-color="#414141" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_909_33931"
|
||||
x1="306.809"
|
||||
y1="24.8051"
|
||||
x2="338.779"
|
||||
y2="78.896"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#383838" />
|
||||
<stop offset="0.260417" stop-color="#2A2A2A" />
|
||||
<stop offset="0.619792" stop-color="#393939" />
|
||||
<stop offset="1" stop-color="#414141" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_909_33931"
|
||||
x1="395.374"
|
||||
y1="33.5171"
|
||||
x2="416.688"
|
||||
y2="69.5777"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#383838" />
|
||||
<stop offset="0.260417" stop-color="#2A2A2A" />
|
||||
<stop offset="0.619792" stop-color="#393939" />
|
||||
<stop offset="1" stop-color="#414141" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_909_33931">
|
||||
<rect
|
||||
x="0.505859"
|
||||
y="30.9414"
|
||||
width="40"
|
||||
height="40"
|
||||
rx="6.06061"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_909_33931">
|
||||
<rect
|
||||
x="72.5059"
|
||||
y="20.9414"
|
||||
width="60"
|
||||
height="60"
|
||||
rx="9.09091"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_909_33931">
|
||||
<rect
|
||||
x="164.506"
|
||||
y="0.941406"
|
||||
width="100"
|
||||
height="100"
|
||||
rx="15.1515"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip3_909_33931">
|
||||
<rect
|
||||
x="296.506"
|
||||
y="20.9414"
|
||||
width="60"
|
||||
height="60"
|
||||
rx="9.09091"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip4_909_33931">
|
||||
<rect
|
||||
width="36.0399"
|
||||
height="36.0399"
|
||||
fill="white"
|
||||
transform="translate(308.519 32.9546)"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip5_909_33931">
|
||||
<rect
|
||||
x="388.506"
|
||||
y="30.9414"
|
||||
width="40"
|
||||
height="40"
|
||||
rx="6.06061"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>`;
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const promptRoot = style({
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
});
|
||||
export const promptTitle = style({
|
||||
fontSize: cssVar('fontH4'),
|
||||
fontWeight: '600',
|
||||
marginBottom: 48,
|
||||
});
|
||||
export const promptArt = style({
|
||||
marginBottom: 68,
|
||||
});
|
||||
export const promptWarning = style({
|
||||
backgroundColor: cssVar('backgroundTertiaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: 14,
|
||||
padding: 10,
|
||||
borderRadius: 8,
|
||||
});
|
||||
export const promptWarningTitle = style({
|
||||
color: cssVar('errorColor'),
|
||||
fontWeight: 600,
|
||||
});
|
||||
export const spacer = style({
|
||||
flexGrow: 1,
|
||||
minHeight: 12,
|
||||
});
|
||||
export const promptDisclaimer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 32,
|
||||
gap: 4,
|
||||
});
|
||||
|
||||
export const settingsContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 24,
|
||||
});
|
||||
|
||||
export const promptDisclaimerConfirm = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
export const switchRow = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
});
|
||||
export const subHeader = style({
|
||||
fontWeight: '600',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
marginBottom: 8,
|
||||
});
|
||||
|
||||
export const rowContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
});
|
||||
export const description = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
// 2 lines
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
width: '100%',
|
||||
});
|
||||
export const feedback = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
gap: 8,
|
||||
});
|
||||
|
||||
export const arrowRightIcon = style({
|
||||
marginLeft: 'auto',
|
||||
marginRight: 0,
|
||||
});
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
import { Button, Checkbox, Loading, Switch, Tooltip } from '@affine/component';
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
ArrowRightSmallIcon,
|
||||
DiscordIcon,
|
||||
EmailIcon,
|
||||
GithubIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
AFFINE_FLAGS,
|
||||
FeatureFlagService,
|
||||
type Flag,
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { Suspense, useCallback, useState } from 'react';
|
||||
|
||||
import { ExperimentalFeatureArts } from './arts';
|
||||
import * as styles from './index.css';
|
||||
|
||||
const ExperimentalFeaturesPrompt = ({
|
||||
onConfirm,
|
||||
}: {
|
||||
onConfirm: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const onChange: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
checked: boolean
|
||||
) => void = useCallback((_, checked) => {
|
||||
setChecked(checked);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.promptRoot} data-testid="experimental-prompt">
|
||||
<div className={styles.promptTitle}>
|
||||
{t[
|
||||
'com.affine.settings.workspace.experimental-features.prompt-header'
|
||||
]()}
|
||||
</div>
|
||||
<div className={styles.promptArt}>
|
||||
<ExperimentalFeatureArts />
|
||||
</div>
|
||||
<div className={styles.promptWarning}>
|
||||
<div className={styles.promptWarningTitle}>
|
||||
{t[
|
||||
'com.affine.settings.workspace.experimental-features.prompt-warning-title'
|
||||
]()}
|
||||
</div>
|
||||
{t[
|
||||
'com.affine.settings.workspace.experimental-features.prompt-warning'
|
||||
]()}
|
||||
</div>
|
||||
|
||||
<div className={styles.spacer} />
|
||||
|
||||
<label className={styles.promptDisclaimer}>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
data-testid="experimental-prompt-disclaimer"
|
||||
/>
|
||||
{t[
|
||||
'com.affine.settings.workspace.experimental-features.prompt-disclaimer'
|
||||
]()}
|
||||
</label>
|
||||
|
||||
<div className={styles.promptDisclaimerConfirm}>
|
||||
<Button
|
||||
disabled={!checked}
|
||||
onClick={onConfirm}
|
||||
variant="primary"
|
||||
data-testid="experimental-confirm-button"
|
||||
>
|
||||
{t[
|
||||
'com.affine.settings.workspace.experimental-features.get-started'
|
||||
]()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FeedbackIcon = ({ type }: { type: Flag['feedbackType'] }) => {
|
||||
switch (type) {
|
||||
case 'discord':
|
||||
return <DiscordIcon fontSize={16} />;
|
||||
case 'email':
|
||||
return <EmailIcon fontSize={16} />;
|
||||
case 'github':
|
||||
return <GithubIcon fontSize={16} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const feedbackLink: Record<NonNullable<Flag['feedbackType']>, string> = {
|
||||
discord: 'https://discord.gg/whd5mjYqVw',
|
||||
email: 'mailto:support@toeverything.info',
|
||||
github: 'https://github.com/toeverything/AFFiNE/issues',
|
||||
};
|
||||
|
||||
const ExperimentalFeaturesItem = ({ flag }: { flag: Flag }) => {
|
||||
const value = useLiveData(flag.$);
|
||||
const onChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
flag.set(checked);
|
||||
},
|
||||
[flag]
|
||||
);
|
||||
const link = flag.feedbackType
|
||||
? flag.feedbackLink
|
||||
? flag.feedbackLink
|
||||
: feedbackLink[flag.feedbackType]
|
||||
: undefined;
|
||||
|
||||
if (flag.configurable === false || flag.hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.rowContainer}>
|
||||
<div className={styles.switchRow}>
|
||||
{flag.displayName}
|
||||
<Switch checked={value} onChange={onChange} />
|
||||
</div>
|
||||
{!!flag.description && (
|
||||
<Tooltip content={flag.description}>
|
||||
<div className={styles.description}>{flag.description}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!!flag.feedbackType && (
|
||||
<a
|
||||
className={styles.feedback}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<FeedbackIcon type={flag.feedbackType} />
|
||||
<span>Discussion about this feature</span>
|
||||
<ArrowRightSmallIcon
|
||||
fontSize={20}
|
||||
className={styles.arrowRightIcon}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExperimentalFeaturesMain = () => {
|
||||
const t = useI18n();
|
||||
const { featureFlagService } = useServices({ FeatureFlagService });
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t[
|
||||
'com.affine.settings.workspace.experimental-features.header.plugins'
|
||||
]()}
|
||||
subtitle={t[
|
||||
'com.affine.settings.workspace.experimental-features.header.subtitle'
|
||||
]()}
|
||||
/>
|
||||
<div
|
||||
className={styles.settingsContainer}
|
||||
data-testid="experimental-settings"
|
||||
>
|
||||
{Object.keys(AFFINE_FLAGS).map(key => (
|
||||
<ExperimentalFeaturesItem
|
||||
key={key}
|
||||
flag={featureFlagService.flags[key as keyof AFFINE_FLAGS]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO(@Peng): save to workspace meta instead?
|
||||
const experimentalFeaturesDisclaimerAtom = atomWithStorage(
|
||||
'affine:experimental-features-disclaimer',
|
||||
false
|
||||
);
|
||||
|
||||
export const ExperimentalFeatures = () => {
|
||||
const [enabled, setEnabled] = useAtom(experimentalFeaturesDisclaimerAtom);
|
||||
const handleConfirm = useAsyncCallback(async () => {
|
||||
setEnabled(true);
|
||||
}, [setEnabled]);
|
||||
if (!enabled) {
|
||||
return <ExperimentalFeaturesPrompt onConfirm={handleConfirm} />;
|
||||
} else {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<ExperimentalFeaturesMain />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
};
|
||||
-843
@@ -1,843 +0,0 @@
|
||||
export default `<svg
|
||||
width="429"
|
||||
height="100"
|
||||
viewBox="0 0 429 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g opacity="0.6" filter="url(#filter0_i_907_33313)">
|
||||
<g clip-path="url(#clip0_907_33313)">
|
||||
<rect
|
||||
x="0.624512"
|
||||
y="30"
|
||||
width="40"
|
||||
height="40"
|
||||
rx="6.06061"
|
||||
fill="#F8F8F8"
|
||||
/>
|
||||
<g filter="url(#filter1_d_907_33313)">
|
||||
<rect
|
||||
x="2.44263"
|
||||
y="31.8181"
|
||||
width="36.3636"
|
||||
height="36.3636"
|
||||
rx="3.96118"
|
||||
fill="url(#paint0_linear_907_33313)"
|
||||
/>
|
||||
<rect
|
||||
x="2.59414"
|
||||
y="31.9697"
|
||||
width="36.0606"
|
||||
height="36.0606"
|
||||
rx="3.80966"
|
||||
stroke="#E4E4E4"
|
||||
stroke-width="0.30303"
|
||||
/>
|
||||
<g filter="url(#filter2_i_907_33313)">
|
||||
<path
|
||||
d="M29.8023 40H20.1707C20.1707 42.4001 22.1166 44.346 24.5167 44.346H26.2905V46.0605C26.2925 48.4552 28.2311 50.3965 30.6238 50.4031H30.6245V40.8383C30.6245 40.8369 30.6245 40.8363 30.6245 40.8349C30.6245 40.3781 30.2577 40.0073 29.8029 40.0007H29.8023V40ZM25.0368 44.7974H15.4053C15.4059 47.1975 17.3512 49.1427 19.7512 49.1427H21.5251V50.8579C21.5271 53.2573 23.4717 55.2012 25.8711 55.2032V45.6324C25.8711 45.1716 25.4976 44.7981 25.0368 44.7981V44.7974ZM20.2674 49.5942H10.6245C10.6258 51.9949 12.5724 53.9402 14.9731 53.9402C14.9772 53.9402 14.9812 53.9402 14.9852 53.9402H16.7597V55.654C16.7597 58.0534 18.7043 59.9987 21.1037 60V50.4318C21.1037 49.969 20.7289 49.5942 20.2661 49.5942H20.2674Z"
|
||||
fill="#E2E5E6"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g opacity="0.8" filter="url(#filter3_i_907_33313)">
|
||||
<g clip-path="url(#clip1_907_33313)">
|
||||
<rect
|
||||
x="72.6245"
|
||||
y="20"
|
||||
width="60"
|
||||
height="60"
|
||||
rx="9.09091"
|
||||
fill="#F8F8F8"
|
||||
/>
|
||||
<g filter="url(#filter4_d_907_33313)">
|
||||
<rect
|
||||
x="75.3518"
|
||||
y="22.7272"
|
||||
width="54.5455"
|
||||
height="54.5455"
|
||||
rx="5.94177"
|
||||
fill="url(#paint1_linear_907_33313)"
|
||||
/>
|
||||
<rect
|
||||
x="75.5791"
|
||||
y="22.9545"
|
||||
width="54.0909"
|
||||
height="54.0909"
|
||||
rx="5.7145"
|
||||
stroke="#E4E4E4"
|
||||
stroke-width="0.454545"
|
||||
/>
|
||||
<g filter="url(#filter5_i_907_33313)">
|
||||
<path
|
||||
d="M107.82 46.3182H101.633V34.25H107.82C111.159 34.25 113.875 36.9563 113.875 40.2834C113.875 43.6105 111.159 46.3182 107.82 46.3182ZM103.617 44.3415H107.82C110.065 44.3415 111.891 42.5208 111.891 40.2848C111.891 38.0488 110.064 36.228 107.82 36.228H103.617V44.3415ZM103.617 46.3182H97.4309C94.092 46.3182 91.3761 43.6119 91.3761 40.2848C91.3761 36.9576 94.092 34.25 97.4309 34.25H103.618V46.3182H103.617ZM97.4309 36.2266C95.1857 36.2266 93.3598 38.0474 93.3598 40.2834C93.3598 42.5194 95.1857 44.3415 97.4309 44.3415H101.634V36.2266H97.4309ZM103.617 56.4084H97.4309C94.092 56.4084 91.3761 53.7021 91.3761 50.375C91.3761 47.0478 94.092 44.3415 97.4309 44.3415H103.618V56.4084H103.617ZM97.4309 46.3182C95.1857 46.3182 93.3598 48.139 93.3598 50.375C93.3598 52.611 95.187 54.4318 97.4309 54.4318H101.634V46.3182H97.4309ZM97.4633 66.5C94.1069 66.5 91.3748 63.7937 91.3748 60.4665C91.3748 57.1394 94.0907 54.4331 97.4296 54.4331H103.617V60.4007C103.617 63.7641 100.856 66.5 97.4633 66.5ZM97.4309 56.4084C96.3516 56.4098 95.317 56.8377 94.5538 57.5982C93.7906 58.3587 93.3612 59.3897 93.3598 60.4652C93.3598 62.7025 95.2005 64.522 97.4647 64.522C99.7639 64.522 101.636 62.673 101.636 60.3993V56.4084H97.4309ZM107.82 56.4084H107.688C104.349 56.4084 101.633 53.7021 101.633 50.375C101.633 47.0478 104.349 44.3415 107.688 44.3415H107.82C111.159 44.3415 113.875 47.0478 113.875 50.375C113.875 53.7021 111.159 56.4084 107.82 56.4084ZM107.689 46.3182C105.444 46.3182 103.618 48.139 103.618 50.375C103.618 52.611 105.445 54.4318 107.689 54.4318H107.821C110.067 54.4318 111.892 52.611 111.892 50.375C111.892 48.139 110.064 46.3182 107.82 46.3182H107.689Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M107.82 46.3182H101.633V34.25H107.82C111.159 34.25 113.875 36.9563 113.875 40.2834C113.875 43.6105 111.159 46.3182 107.82 46.3182ZM103.617 44.3415H107.82C110.065 44.3415 111.891 42.5208 111.891 40.2848C111.891 38.0488 110.064 36.228 107.82 36.228H103.617V44.3415ZM103.617 46.3182H97.4309C94.092 46.3182 91.3761 43.6119 91.3761 40.2848C91.3761 36.9576 94.092 34.25 97.4309 34.25H103.618V46.3182H103.617ZM97.4309 36.2266C95.1857 36.2266 93.3598 38.0474 93.3598 40.2834C93.3598 42.5194 95.1857 44.3415 97.4309 44.3415H101.634V36.2266H97.4309ZM103.617 56.4084H97.4309C94.092 56.4084 91.3761 53.7021 91.3761 50.375C91.3761 47.0478 94.092 44.3415 97.4309 44.3415H103.618V56.4084H103.617ZM97.4309 46.3182C95.1857 46.3182 93.3598 48.139 93.3598 50.375C93.3598 52.611 95.187 54.4318 97.4309 54.4318H101.634V46.3182H97.4309ZM97.4633 66.5C94.1069 66.5 91.3748 63.7937 91.3748 60.4665C91.3748 57.1394 94.0907 54.4331 97.4296 54.4331H103.617V60.4007C103.617 63.7641 100.856 66.5 97.4633 66.5ZM97.4309 56.4084C96.3516 56.4098 95.317 56.8377 94.5538 57.5982C93.7906 58.3587 93.3612 59.3897 93.3598 60.4652C93.3598 62.7025 95.2005 64.522 97.4647 64.522C99.7639 64.522 101.636 62.673 101.636 60.3993V56.4084H97.4309ZM107.82 56.4084H107.688C104.349 56.4084 101.633 53.7021 101.633 50.375C101.633 47.0478 104.349 44.3415 107.688 44.3415H107.82C111.159 44.3415 113.875 47.0478 113.875 50.375C113.875 53.7021 111.159 56.4084 107.82 56.4084ZM107.689 46.3182C105.444 46.3182 103.618 48.139 103.618 50.375C103.618 52.611 105.445 54.4318 107.689 54.4318H107.821C110.067 54.4318 111.892 52.611 111.892 50.375C111.892 48.139 110.064 46.3182 107.82 46.3182H107.689Z"
|
||||
fill="#E2E5E6"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g filter="url(#filter6_i_907_33313)">
|
||||
<g clip-path="url(#clip2_907_33313)">
|
||||
<rect x="164.625" width="100" height="100" rx="15.1515" fill="#F8F8F8" />
|
||||
<g filter="url(#filter7_d_907_33313)">
|
||||
<rect
|
||||
x="169.17"
|
||||
y="4.54544"
|
||||
width="90.9091"
|
||||
height="90.9091"
|
||||
rx="9.90295"
|
||||
fill="url(#paint2_linear_907_33313)"
|
||||
/>
|
||||
<rect
|
||||
x="169.549"
|
||||
y="4.92423"
|
||||
width="90.1515"
|
||||
height="90.1515"
|
||||
rx="9.52416"
|
||||
stroke="#E4E4E4"
|
||||
stroke-width="0.757576"
|
||||
/>
|
||||
<g filter="url(#filter8_i_907_33313)">
|
||||
<path
|
||||
d="M194.542 40.5226C194.542 38.6884 195.271 36.9293 196.568 35.6323C197.865 34.3353 199.624 33.6066 201.458 33.6066H208.036C208.72 33.6066 209.142 32.8943 208.879 32.2626C208.747 31.9521 208.642 31.6304 208.566 31.3013C207.803 27.8733 210.392 24.3853 214.138 24.3853C217.886 24.3853 220.473 27.8733 219.71 31.3013C219.634 31.6304 219.529 31.9521 219.396 32.2626C219.131 32.8943 219.555 33.6066 220.24 33.6066H224.512C226.346 33.6066 228.105 34.3353 229.402 35.6323C230.699 36.9293 231.428 38.6884 231.428 40.5226V44.7944C231.428 45.4791 232.138 45.9009 232.772 45.6381C233.083 45.509 233.401 45.3984 233.733 45.3246C237.161 44.5615 240.649 47.1481 240.649 50.8966C240.649 54.645 237.161 57.2316 233.733 56.4686C233.404 56.3927 233.082 56.2878 232.772 56.155C232.14 55.8899 231.428 56.3141 231.428 56.9988V63.5759C231.428 65.4101 230.699 67.1692 229.402 68.4662C228.105 69.7632 226.346 70.4919 224.512 70.4919H220.148C219.484 70.4919 219.057 69.821 219.249 69.1871C219.343 68.8643 219.415 68.5323 219.445 68.1865C219.506 67.4524 219.414 66.7136 219.175 66.0168C218.936 65.3199 218.556 64.6802 218.057 64.1381C217.558 63.596 216.952 63.1633 216.278 62.8673C215.603 62.5713 214.874 62.4184 214.138 62.4184C213.401 62.4184 212.672 62.5713 211.998 62.8673C211.323 63.1633 210.718 63.596 210.219 64.1381C209.72 64.6802 209.339 65.3199 209.1 66.0168C208.861 66.7136 208.77 67.4524 208.831 68.1865C208.861 68.5323 208.932 68.8643 209.029 69.1871C209.218 69.821 208.792 70.4919 208.13 70.4919H201.458C199.624 70.4919 197.865 69.7632 196.568 68.4662C195.271 67.1692 194.542 65.4101 194.542 63.5759V56.9066C194.542 56.2426 195.213 55.8162 195.847 56.0075C196.17 56.102 196.502 56.1735 196.848 56.2034C197.582 56.2647 198.321 56.173 199.018 55.9341C199.714 55.6952 200.354 55.3143 200.896 54.8156C201.438 54.3168 201.871 53.711 202.167 53.0364C202.463 52.3619 202.616 51.6332 202.616 50.8966C202.616 50.1599 202.463 49.4313 202.167 48.7567C201.871 48.0822 201.438 47.4764 200.896 46.9776C200.354 46.4788 199.714 46.098 199.018 45.8591C198.321 45.6202 197.582 45.5285 196.848 45.5897C196.502 45.6197 196.17 45.6912 195.847 45.788C195.213 45.977 194.542 45.5505 194.542 44.8889V40.5226Z"
|
||||
fill="#DEE2E3"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g opacity="0.8" filter="url(#filter9_i_907_33313)">
|
||||
<g clip-path="url(#clip3_907_33313)">
|
||||
<rect
|
||||
x="296.625"
|
||||
y="20"
|
||||
width="60"
|
||||
height="60"
|
||||
rx="9.09091"
|
||||
fill="#F8F8F8"
|
||||
/>
|
||||
<g filter="url(#filter10_d_907_33313)">
|
||||
<rect
|
||||
x="299.352"
|
||||
y="22.7272"
|
||||
width="54.5455"
|
||||
height="54.5455"
|
||||
rx="5.94177"
|
||||
fill="url(#paint3_linear_907_33313)"
|
||||
/>
|
||||
<rect
|
||||
x="299.579"
|
||||
y="22.9545"
|
||||
width="54.0909"
|
||||
height="54.0909"
|
||||
rx="5.7145"
|
||||
stroke="#E4E4E4"
|
||||
stroke-width="0.454545"
|
||||
/>
|
||||
<g
|
||||
clip-path="url(#clip4_907_33313)"
|
||||
filter="url(#filter11_i_907_33313)"
|
||||
>
|
||||
<path
|
||||
d="M326.658 32.0132C316.708 32.0132 308.638 40.0817 308.638 50.0332C308.638 57.995 313.801 64.7495 320.962 67.1327C321.862 67.2994 322.153 66.7407 322.153 66.2662V62.9115C317.14 64.0017 316.097 60.7851 316.097 60.7851C315.277 58.7023 314.095 58.1482 314.095 58.1482C312.46 57.0295 314.22 57.0535 314.22 57.0535C316.029 57.1796 316.981 58.9111 316.981 58.9111C318.588 61.6651 321.196 60.8692 322.225 60.4082C322.386 59.2444 322.853 58.4485 323.369 57.9995C319.367 57.5415 315.16 55.9963 315.16 49.0932C315.16 47.1245 315.864 45.5177 317.016 44.2563C316.83 43.8013 316.212 41.9678 317.191 39.487C317.191 39.487 318.705 39.0035 322.148 41.3341C323.586 40.9346 325.126 40.7349 326.658 40.7274C328.19 40.7349 329.732 40.9346 331.172 41.3341C334.612 39.0035 336.123 39.487 336.123 39.487C337.103 41.9693 336.486 43.8028 336.3 44.2563C337.456 45.5177 338.155 47.126 338.155 49.0932C338.155 56.0143 333.939 57.5385 329.927 57.9845C330.573 58.5432 331.163 59.6394 331.163 61.3212V66.2662C331.163 66.7452 331.451 67.3084 332.366 67.1312C339.521 64.745 344.678 57.992 344.678 50.0332C344.678 40.0817 336.609 32.0132 326.658 32.0132Z"
|
||||
fill="#E2E5E6"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g opacity="0.6" filter="url(#filter12_i_907_33313)">
|
||||
<g clip-path="url(#clip5_907_33313)">
|
||||
<rect
|
||||
x="388.625"
|
||||
y="30"
|
||||
width="40"
|
||||
height="40"
|
||||
rx="6.06061"
|
||||
fill="#F8F8F8"
|
||||
/>
|
||||
<g filter="url(#filter13_d_907_33313)">
|
||||
<rect
|
||||
x="390.443"
|
||||
y="31.8181"
|
||||
width="36.3636"
|
||||
height="36.3636"
|
||||
rx="3.96118"
|
||||
fill="url(#paint4_linear_907_33313)"
|
||||
/>
|
||||
<rect
|
||||
x="390.594"
|
||||
y="31.9697"
|
||||
width="36.0606"
|
||||
height="36.0606"
|
||||
rx="3.80966"
|
||||
stroke="#E4E4E4"
|
||||
stroke-width="0.30303"
|
||||
/>
|
||||
<g filter="url(#filter14_i_907_33313)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M414.974 43.636C411.473 40.135 405.805 40.1214 402.287 43.5952L415.015 56.3228C418.489 52.8049 418.475 47.1371 414.974 43.636ZM400.492 46.1099L412.5 58.1186C413.123 57.8201 413.719 57.4458 414.274 56.9956L401.615 44.3365C401.165 44.8912 400.79 45.487 400.492 46.1099ZM409.117 58.9858L399.625 49.4938C399.67 48.6883 399.822 47.8877 400.083 47.1155L411.495 58.5273C410.723 58.7879 409.922 58.9407 409.117 58.9858ZM402.246 56.364C400.729 54.8468 399.867 52.9227 399.66 50.9431L407.667 58.9508C405.688 58.7434 403.763 57.8811 402.246 56.364Z"
|
||||
fill="#DEE2E3"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_i_907_33313"
|
||||
x="0.624512"
|
||||
y="29.798"
|
||||
width="40"
|
||||
height="40.202"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.20202" />
|
||||
<feGaussianBlur stdDeviation="0.707071" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_d_907_33313"
|
||||
x="0.927476"
|
||||
y="30.808"
|
||||
width="39.3938"
|
||||
height="39.394"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.505051" />
|
||||
<feGaussianBlur stdDeviation="0.757576" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_907_33313"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_907_33313"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_i_907_33313"
|
||||
x="8.6333"
|
||||
y="38.0088"
|
||||
width="24.2725"
|
||||
height="24.2725"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.245902" dy="0.245902" />
|
||||
<feGaussianBlur stdDeviation="0.327869" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter3_i_907_33313"
|
||||
x="72.6245"
|
||||
y="19.697"
|
||||
width="60"
|
||||
height="60.303"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.30303" />
|
||||
<feGaussianBlur stdDeviation="1.06061" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter4_d_907_33313"
|
||||
x="73.0791"
|
||||
y="21.2121"
|
||||
width="59.0909"
|
||||
height="59.0909"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.757576" />
|
||||
<feGaussianBlur stdDeviation="1.13636" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_907_33313"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_907_33313"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter5_i_907_33313"
|
||||
x="84.6379"
|
||||
y="32.0132"
|
||||
width="36.4089"
|
||||
height="36.4088"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.368852" dy="0.368852" />
|
||||
<feGaussianBlur stdDeviation="0.491803" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter6_i_907_33313"
|
||||
x="164.625"
|
||||
y="-0.50505"
|
||||
width="100"
|
||||
height="100.505"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.50505" />
|
||||
<feGaussianBlur stdDeviation="1.76768" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter7_d_907_33313"
|
||||
x="165.382"
|
||||
y="2.02019"
|
||||
width="98.4849"
|
||||
height="98.4848"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1.26263" />
|
||||
<feGaussianBlur stdDeviation="1.89394" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_907_33313"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_907_33313"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter8_i_907_33313"
|
||||
x="194.542"
|
||||
y="24.3853"
|
||||
width="46.7212"
|
||||
height="46.7213"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.614754" dy="0.614754" />
|
||||
<feGaussianBlur stdDeviation="0.819672" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter9_i_907_33313"
|
||||
x="296.625"
|
||||
y="19.697"
|
||||
width="60"
|
||||
height="60.303"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.30303" />
|
||||
<feGaussianBlur stdDeviation="1.06061" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter10_d_907_33313"
|
||||
x="297.079"
|
||||
y="21.2121"
|
||||
width="59.0909"
|
||||
height="59.0909"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.757576" />
|
||||
<feGaussianBlur stdDeviation="1.13636" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_907_33313"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_907_33313"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter11_i_907_33313"
|
||||
x="308.638"
|
||||
y="32.0132"
|
||||
width="36.4089"
|
||||
height="36.4088"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.368852" dy="0.368852" />
|
||||
<feGaussianBlur stdDeviation="0.491803" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter12_i_907_33313"
|
||||
x="388.625"
|
||||
y="29.798"
|
||||
width="40"
|
||||
height="40.202"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.20202" />
|
||||
<feGaussianBlur stdDeviation="0.707071" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter13_d_907_33313"
|
||||
x="388.927"
|
||||
y="30.808"
|
||||
width="39.3938"
|
||||
height="39.394"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.505051" />
|
||||
<feGaussianBlur stdDeviation="0.757576" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_907_33313"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_907_33313"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter14_i_907_33313"
|
||||
x="396.633"
|
||||
y="38.0088"
|
||||
width="24.2725"
|
||||
height="24.2725"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.245902" dy="0.245902" />
|
||||
<feGaussianBlur stdDeviation="0.327869" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_907_33313"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_907_33313"
|
||||
x1="7.49313"
|
||||
y1="32.5757"
|
||||
x2="28.8063"
|
||||
y2="68.6363"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F8F8F8" />
|
||||
<stop offset="0.348555" stop-color="white" />
|
||||
<stop offset="0.672956" stop-color="white" />
|
||||
<stop offset="1" stop-color="#FAFAFA" />
|
||||
<stop offset="1" stop-color="#F8F8F8" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_907_33313"
|
||||
x1="82.9276"
|
||||
y1="23.8636"
|
||||
x2="114.897"
|
||||
y2="77.9545"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F8F8F8" />
|
||||
<stop offset="0.348555" stop-color="white" />
|
||||
<stop offset="0.672956" stop-color="white" />
|
||||
<stop offset="1" stop-color="#FAFAFA" />
|
||||
<stop offset="1" stop-color="#F8F8F8" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_907_33313"
|
||||
x1="181.796"
|
||||
y1="6.43938"
|
||||
x2="235.079"
|
||||
y2="96.5909"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F8F8F8" />
|
||||
<stop offset="0.348555" stop-color="white" />
|
||||
<stop offset="0.672956" stop-color="white" />
|
||||
<stop offset="1" stop-color="#FAFAFA" />
|
||||
<stop offset="1" stop-color="#F8F8F8" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_907_33313"
|
||||
x1="306.928"
|
||||
y1="23.8636"
|
||||
x2="338.897"
|
||||
y2="77.9545"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F8F8F8" />
|
||||
<stop offset="0.348555" stop-color="white" />
|
||||
<stop offset="0.672956" stop-color="white" />
|
||||
<stop offset="1" stop-color="#FAFAFA" />
|
||||
<stop offset="1" stop-color="#F8F8F8" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_907_33313"
|
||||
x1="395.493"
|
||||
y1="32.5757"
|
||||
x2="416.806"
|
||||
y2="68.6363"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F8F8F8" />
|
||||
<stop offset="0.348555" stop-color="white" />
|
||||
<stop offset="0.672956" stop-color="white" />
|
||||
<stop offset="1" stop-color="#FAFAFA" />
|
||||
<stop offset="1" stop-color="#F8F8F8" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_907_33313">
|
||||
<rect
|
||||
x="0.624512"
|
||||
y="30"
|
||||
width="40"
|
||||
height="40"
|
||||
rx="6.06061"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_907_33313">
|
||||
<rect
|
||||
x="72.6245"
|
||||
y="20"
|
||||
width="60"
|
||||
height="60"
|
||||
rx="9.09091"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_907_33313">
|
||||
<rect x="164.625" width="100" height="100" rx="15.1515" fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="clip3_907_33313">
|
||||
<rect
|
||||
x="296.625"
|
||||
y="20"
|
||||
width="60"
|
||||
height="60"
|
||||
rx="9.09091"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip4_907_33313">
|
||||
<rect
|
||||
width="36.0399"
|
||||
height="36.0399"
|
||||
fill="white"
|
||||
transform="translate(308.638 32.0132)"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip5_907_33313">
|
||||
<rect
|
||||
x="388.625"
|
||||
y="30"
|
||||
width="40"
|
||||
height="40"
|
||||
rx="6.06061"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>`;
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export const UpgradeIcon = ({ width, height }: SVGProps<SVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
className="icon"
|
||||
width={width || 16}
|
||||
height={height || 16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.5 8C1.5 4.41022 4.41022 1.5 8 1.5C11.5898 1.5 14.5 4.41022 14.5 8C14.5 11.5898 11.5898 14.5 8 14.5C4.41022 14.5 1.5 11.5898 1.5 8ZM8 2.5C4.96251 2.5 2.5 4.96251 2.5 8C2.5 11.0375 4.96251 13.5 8 13.5C11.0375 13.5 13.5 11.0375 13.5 8C13.5 4.96251 11.0375 2.5 8 2.5ZM4.91917 7.64645L7.64645 4.91917C7.84171 4.72391 8.15829 4.72391 8.35355 4.91917L11.0808 7.64645C11.2761 7.84171 11.2761 8.15829 11.0808 8.35355C10.8856 8.54882 10.569 8.54882 10.3737 8.35355L8.5 6.47983V11C8.5 11.2761 8.27614 11.5 8 11.5C7.72386 11.5 7.5 11.2761 7.5 11V6.47983L5.62628 8.35355C5.43102 8.54882 5.11444 8.54882 4.91917 8.35355C4.72391 8.15829 4.72391 7.84171 4.91917 7.64645Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const PaymentIcon = ({ width, height }: SVGProps<SVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
className="icon"
|
||||
width={width || 16}
|
||||
height={height || 16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.5 4.66634C1.5 3.65382 2.32081 2.83301 3.33333 2.83301H12.6667C13.6792 2.83301 14.5 3.65382 14.5 4.66634V11.333C14.5 12.3455 13.6792 13.1663 12.6667 13.1663H3.33333C2.32081 13.1663 1.5 12.3455 1.5 11.333V4.66634ZM3.33333 3.83301C2.8731 3.83301 2.5 4.2061 2.5 4.66634V5.49967H13.5V4.66634C13.5 4.2061 13.1269 3.83301 12.6667 3.83301H3.33333ZM13.5 6.49967H2.5V11.333C2.5 11.7932 2.8731 12.1663 3.33333 12.1663H12.6667C13.1269 12.1663 13.5 11.7932 13.5 11.333V6.49967ZM9.5 9.99967C9.5 9.72353 9.72386 9.49967 10 9.49967H12C12.2761 9.49967 12.5 9.72353 12.5 9.99967C12.5 10.2758 12.2761 10.4997 12 10.4997H10C9.72386 10.4997 9.5 10.2758 9.5 9.99967Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,143 +0,0 @@
|
||||
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
AppearanceIcon,
|
||||
ExperimentIcon,
|
||||
InformationIcon,
|
||||
KeyboardIcon,
|
||||
PenIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import type { ReactElement, SVGProps } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { AuthService, ServerConfigService } from '../../../../modules/cloud';
|
||||
import type { GeneralSettingKey } from '../types';
|
||||
import { AboutAffine } from './about';
|
||||
import { AppearanceSettings } from './appearance';
|
||||
import { BillingSettings } from './billing';
|
||||
import { EditorSettings } from './editor';
|
||||
import { ExperimentalFeatures } from './experimental-features';
|
||||
import { PaymentIcon, UpgradeIcon } from './icons';
|
||||
import { AFFiNEPricingPlans } from './plans';
|
||||
import { Shortcuts } from './shortcuts';
|
||||
|
||||
interface GeneralSettingListItem {
|
||||
key: GeneralSettingKey;
|
||||
title: string;
|
||||
icon: (props: SVGProps<SVGSVGElement>) => ReactElement;
|
||||
testId: string;
|
||||
}
|
||||
|
||||
export type GeneralSettingList = GeneralSettingListItem[];
|
||||
|
||||
export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
const t = useI18n();
|
||||
const {
|
||||
authService,
|
||||
serverConfigService,
|
||||
userFeatureService,
|
||||
featureFlagService,
|
||||
} = useServices({
|
||||
AuthService,
|
||||
ServerConfigService,
|
||||
UserFeatureService,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const status = useLiveData(authService.session.status$);
|
||||
const hasPaymentFeature = useLiveData(
|
||||
serverConfigService.serverConfig.features$.map(f => f?.payment)
|
||||
);
|
||||
const enableEditorSettings = useLiveData(
|
||||
featureFlagService.flags.enable_editor_settings.$
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
userFeatureService.userFeature.revalidate();
|
||||
}, [userFeatureService]);
|
||||
|
||||
const settings: GeneralSettingListItem[] = [
|
||||
{
|
||||
key: 'appearance',
|
||||
title: t['com.affine.settings.appearance'](),
|
||||
icon: AppearanceIcon,
|
||||
testId: 'appearance-panel-trigger',
|
||||
},
|
||||
{
|
||||
key: 'shortcuts',
|
||||
title: t['com.affine.keyboardShortcuts.title'](),
|
||||
icon: KeyboardIcon,
|
||||
testId: 'shortcuts-panel-trigger',
|
||||
},
|
||||
{
|
||||
key: 'about',
|
||||
title: t['com.affine.aboutAFFiNE.title'](),
|
||||
icon: InformationIcon,
|
||||
testId: 'about-panel-trigger',
|
||||
},
|
||||
];
|
||||
if (enableEditorSettings) {
|
||||
// add editor settings to second position
|
||||
settings.splice(1, 0, {
|
||||
key: 'editor',
|
||||
title: t['com.affine.settings.editorSettings'](),
|
||||
icon: PenIcon,
|
||||
testId: 'editor-panel-trigger',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasPaymentFeature) {
|
||||
settings.splice(3, 0, {
|
||||
key: 'plans',
|
||||
title: t['com.affine.payment.title'](),
|
||||
icon: UpgradeIcon,
|
||||
testId: 'plans-panel-trigger',
|
||||
});
|
||||
if (status === 'authenticated') {
|
||||
settings.splice(3, 0, {
|
||||
key: 'billing',
|
||||
title: t['com.affine.payment.billing-setting.title'](),
|
||||
icon: PaymentIcon,
|
||||
testId: 'billing-panel-trigger',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
settings.push({
|
||||
key: 'experimental-features',
|
||||
title: t['com.affine.settings.workspace.experimental-features'](),
|
||||
icon: ExperimentIcon,
|
||||
testId: 'experimental-features-trigger',
|
||||
});
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
interface GeneralSettingProps {
|
||||
generalKey: GeneralSettingKey;
|
||||
}
|
||||
|
||||
export const GeneralSetting = ({ generalKey }: GeneralSettingProps) => {
|
||||
switch (generalKey) {
|
||||
case 'shortcuts':
|
||||
return <Shortcuts />;
|
||||
case 'editor':
|
||||
return <EditorSettings />;
|
||||
case 'appearance':
|
||||
return <AppearanceSettings />;
|
||||
case 'about':
|
||||
return <AboutAffine />;
|
||||
case 'plans':
|
||||
return <AFFiNEPricingPlans />;
|
||||
case 'billing':
|
||||
return <BillingSettings />;
|
||||
case 'experimental-features':
|
||||
return <ExperimentalFeatures />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
import { getDowngradeQuestionnaireLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { useDowngradeNotify } from '../../../subscription-landing/notify';
|
||||
import { ConfirmLoadingModal, DowngradeModal } from './modals';
|
||||
|
||||
/**
|
||||
* Cancel action with modal & request
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export const CancelAction = ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
} & PropsWithChildren) => {
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
const proSubscription = useLiveData(subscription.pro$);
|
||||
const authService = useService(AuthService);
|
||||
const downgradeNotify = useDowngradeNotify();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !proSubscription) return;
|
||||
track.$.settingsPanel.plans.cancelSubscription({
|
||||
plan: proSubscription.plan,
|
||||
recurring: proSubscription.recurring,
|
||||
});
|
||||
}, [open, proSubscription]);
|
||||
|
||||
const downgrade = useAsyncCallback(async () => {
|
||||
try {
|
||||
const account = authService.session.account$.value;
|
||||
const prevRecurring = subscription.pro$.value?.recurring;
|
||||
setIsMutating(true);
|
||||
await subscription.cancelSubscription(idempotencyKey);
|
||||
subscription.revalidate();
|
||||
await subscription.isRevalidating$.waitFor(v => !v);
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onOpenChange(false);
|
||||
const proSubscription = subscription.pro$.value;
|
||||
if (proSubscription) {
|
||||
track.$.settingsPanel.plans.confirmCancelingSubscription({
|
||||
plan: proSubscription.plan,
|
||||
recurring: proSubscription.recurring,
|
||||
});
|
||||
}
|
||||
if (account && prevRecurring) {
|
||||
downgradeNotify(
|
||||
getDowngradeQuestionnaireLink({
|
||||
email: account.email ?? '',
|
||||
id: account.id,
|
||||
name: account.info?.name ?? '',
|
||||
plan: SubscriptionPlan.Pro,
|
||||
recurring: prevRecurring,
|
||||
})
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
}, [
|
||||
authService.session.account$.value,
|
||||
subscription,
|
||||
idempotencyKey,
|
||||
onOpenChange,
|
||||
downgradeNotify,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<DowngradeModal
|
||||
open={open}
|
||||
onCancel={downgrade}
|
||||
onOpenChange={onOpenChange}
|
||||
loading={isMutating}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resume payment action with modal & request
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export const ResumeAction = ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
} & PropsWithChildren) => {
|
||||
// allow replay request on network error until component unmount or success
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const resume = useAsyncCallback(async () => {
|
||||
try {
|
||||
setIsMutating(true);
|
||||
await subscription.resumeSubscription(idempotencyKey);
|
||||
subscription.revalidate();
|
||||
await subscription.isRevalidating$.waitFor(v => !v);
|
||||
// refresh idempotency key
|
||||
setIdempotencyKey(nanoid());
|
||||
onOpenChange(false);
|
||||
const proSubscription = subscription.pro$.value;
|
||||
if (proSubscription) {
|
||||
track.$.settingsPanel.plans.confirmResumingSubscription({
|
||||
plan: proSubscription.plan,
|
||||
recurring: proSubscription.recurring,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsMutating(false);
|
||||
}
|
||||
}, [subscription, idempotencyKey, onOpenChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<ConfirmLoadingModal
|
||||
type={'resume'}
|
||||
open={open}
|
||||
onConfirm={resume}
|
||||
onOpenChange={onOpenChange}
|
||||
loading={isMutating}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
import { Button, type ButtonProps, useConfirmModal } from '@affine/component';
|
||||
import { useDowngradeNotify } from '@affine/core/components/affine/subscription-landing/notify';
|
||||
import { getDowngradeQuestionnaireLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const AICancel = (btnProps: ButtonProps) => {
|
||||
const t = useI18n();
|
||||
const [isMutating, setMutating] = useState(false);
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const downgradeNotify = useDowngradeNotify();
|
||||
|
||||
const cancel = useAsyncCallback(async () => {
|
||||
const aiSubscription = subscription.ai$.value;
|
||||
if (aiSubscription) {
|
||||
track.$.settingsPanel.plans.cancelSubscription({
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: aiSubscription.recurring,
|
||||
});
|
||||
}
|
||||
openConfirmModal({
|
||||
title: t['com.affine.payment.ai.action.cancel.confirm.title'](),
|
||||
description:
|
||||
t['com.affine.payment.ai.action.cancel.confirm.description'](),
|
||||
reverseFooter: true,
|
||||
confirmText:
|
||||
t['com.affine.payment.ai.action.cancel.confirm.confirm-text'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'secondary',
|
||||
},
|
||||
cancelText:
|
||||
t['com.affine.payment.ai.action.cancel.confirm.cancel-text'](),
|
||||
cancelButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
setMutating(true);
|
||||
await subscription.cancelSubscription(
|
||||
idempotencyKey,
|
||||
SubscriptionPlan.AI
|
||||
);
|
||||
setIdempotencyKey(nanoid());
|
||||
track.$.settingsPanel.plans.confirmCancelingSubscription({
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: aiSubscription?.recurring,
|
||||
});
|
||||
const account = authService.session.account$.value;
|
||||
const prevRecurring = subscription.ai$.value?.recurring;
|
||||
if (account && prevRecurring) {
|
||||
downgradeNotify(
|
||||
getDowngradeQuestionnaireLink({
|
||||
email: account.email,
|
||||
name: account.info?.name,
|
||||
id: account.id,
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: prevRecurring,
|
||||
})
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setMutating(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [
|
||||
subscription,
|
||||
openConfirmModal,
|
||||
t,
|
||||
idempotencyKey,
|
||||
authService.session.account$.value,
|
||||
downgradeNotify,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={cancel}
|
||||
loading={isMutating}
|
||||
variant="primary"
|
||||
{...btnProps}
|
||||
>
|
||||
{t['com.affine.payment.ai.action.cancel.button-label']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export * from './cancel';
|
||||
export * from './login';
|
||||
export * from './resume';
|
||||
export * from './subscribe';
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { Button, type ButtonProps } from '@affine/component';
|
||||
import { authAtom } from '@affine/core/components/atoms';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const AILogin = (btnProps: ButtonProps) => {
|
||||
const t = useI18n();
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
|
||||
const onClickSignIn = useCallback(() => {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}, [setOpen]);
|
||||
|
||||
return (
|
||||
<Button onClick={onClickSignIn} variant="primary" {...btnProps}>
|
||||
{t['com.affine.payment.ai.action.login.button-label']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { Button, type ButtonProps } from '@affine/component';
|
||||
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
SubscriptionVariant,
|
||||
} from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CheckoutSlot } from '../../checkout-slot';
|
||||
|
||||
export const AIRedeemCodeButton = (btnProps: ButtonProps) => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const onBeforeCheckout = useCallback(() => {
|
||||
track.$.settingsPanel.plans.checkout({
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
});
|
||||
}, []);
|
||||
const checkoutOptions = useMemo(
|
||||
() => ({
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
plan: SubscriptionPlan.AI,
|
||||
variant: SubscriptionVariant.Onetime,
|
||||
coupon: null,
|
||||
successCallbackLink: generateSubscriptionCallbackLink(
|
||||
authService.session.account$.value,
|
||||
SubscriptionPlan.AI,
|
||||
SubscriptionRecurring.Yearly
|
||||
),
|
||||
}),
|
||||
[authService.session.account$.value]
|
||||
);
|
||||
|
||||
return (
|
||||
<CheckoutSlot
|
||||
onBeforeCheckout={onBeforeCheckout}
|
||||
checkoutOptions={checkoutOptions}
|
||||
renderer={props => (
|
||||
<Button variant="primary" {...btnProps} {...props}>
|
||||
{t['com.affine.payment.redeem-code']()}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
type ButtonProps,
|
||||
notify,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const AIResume = (btnProps: ButtonProps) => {
|
||||
const t = useI18n();
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const resume = useAsyncCallback(async () => {
|
||||
const aiSubscription = subscription.ai$.value;
|
||||
if (aiSubscription) {
|
||||
track.$.settingsPanel.plans.resumeSubscription({
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: aiSubscription.recurring,
|
||||
});
|
||||
}
|
||||
|
||||
openConfirmModal({
|
||||
title: t['com.affine.payment.ai.action.resume.confirm.title'](),
|
||||
description:
|
||||
t['com.affine.payment.ai.action.resume.confirm.description'](),
|
||||
confirmText:
|
||||
t['com.affine.payment.ai.action.resume.confirm.confirm-text'](),
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText:
|
||||
t['com.affine.payment.ai.action.resume.confirm.cancel-text'](),
|
||||
onConfirm: async () => {
|
||||
setIsMutating(true);
|
||||
await subscription.resumeSubscription(
|
||||
idempotencyKey,
|
||||
SubscriptionPlan.AI
|
||||
);
|
||||
if (aiSubscription) {
|
||||
track.$.settingsPanel.plans.confirmResumingSubscription({
|
||||
plan: aiSubscription.plan,
|
||||
recurring: aiSubscription.recurring,
|
||||
});
|
||||
}
|
||||
notify({
|
||||
icon: <SingleSelectSelectSolidIcon />,
|
||||
iconColor: cssVar('processingColor'),
|
||||
title:
|
||||
t['com.affine.payment.ai.action.resume.confirm.notify.title'](),
|
||||
message:
|
||||
t['com.affine.payment.ai.action.resume.confirm.notify.msg'](),
|
||||
});
|
||||
setIdempotencyKey(nanoid());
|
||||
},
|
||||
});
|
||||
}, [subscription, openConfirmModal, t, idempotencyKey]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
loading={isMutating}
|
||||
onClick={resume}
|
||||
variant="primary"
|
||||
{...btnProps}
|
||||
>
|
||||
{t['com.affine.payment.ai.action.resume.button-label']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
import { Button, type ButtonProps, Skeleton } from '@affine/component';
|
||||
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CheckoutSlot } from '../../checkout-slot';
|
||||
|
||||
export interface AISubscribeProps extends ButtonProps {
|
||||
displayedFrequency?: 'yearly' | 'monthly' | null;
|
||||
}
|
||||
|
||||
export const AISubscribe = ({
|
||||
displayedFrequency = 'yearly',
|
||||
...btnProps
|
||||
}: AISubscribeProps) => {
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const onBeforeCheckout = useCallback(() => {
|
||||
track.$.settingsPanel.plans.checkout({
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
});
|
||||
}, []);
|
||||
const checkoutOptions = useMemo(
|
||||
() => ({
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
plan: SubscriptionPlan.AI,
|
||||
variant: null,
|
||||
coupon: null,
|
||||
successCallbackLink: generateSubscriptionCallbackLink(
|
||||
authService.session.account$.value,
|
||||
SubscriptionPlan.AI,
|
||||
SubscriptionRecurring.Yearly
|
||||
),
|
||||
}),
|
||||
[authService.session.account$.value]
|
||||
);
|
||||
|
||||
if (!price || !price.yearlyAmount) {
|
||||
return (
|
||||
<Skeleton
|
||||
className={btnProps.className}
|
||||
width={160}
|
||||
height={36}
|
||||
style={{
|
||||
borderRadius: 18,
|
||||
...btnProps.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const priceReadable = `$${(
|
||||
price.yearlyAmount /
|
||||
100 /
|
||||
(displayedFrequency === 'yearly' ? 1 : 12)
|
||||
).toFixed(2)}`;
|
||||
const priceFrequency =
|
||||
displayedFrequency === 'yearly'
|
||||
? t['com.affine.payment.billing-setting.year']()
|
||||
: t['com.affine.payment.billing-setting.month']();
|
||||
|
||||
return (
|
||||
<CheckoutSlot
|
||||
onBeforeCheckout={onBeforeCheckout}
|
||||
checkoutOptions={checkoutOptions}
|
||||
renderer={props => (
|
||||
<Button variant="primary" {...props} {...btnProps}>
|
||||
{btnProps.children ?? `${priceReadable} / ${priceFrequency}`}
|
||||
{displayedFrequency === 'monthly' ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10,
|
||||
opacity: 0.75,
|
||||
letterSpacing: -0.2,
|
||||
paddingLeft: 4,
|
||||
}}
|
||||
>
|
||||
{t['com.affine.payment.ai.subscribe.billed-annually']()}
|
||||
</span>
|
||||
) : null}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const card = style({
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
borderRadius: 16,
|
||||
padding: 36,
|
||||
});
|
||||
|
||||
export const titleBlock = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
marginBottom: 12,
|
||||
});
|
||||
export const titleCaption1 = style({
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('brandColor'),
|
||||
});
|
||||
export const titleCaption2 = style({
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
});
|
||||
export const title = style({
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontWeight: 600,
|
||||
fontSize: '28px',
|
||||
lineHeight: '36px',
|
||||
});
|
||||
|
||||
// action button
|
||||
export const actionBlock = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
alignItems: 'start',
|
||||
marginBottom: 24,
|
||||
});
|
||||
export const actionButtons = style({
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
});
|
||||
export const purchaseButton = style({
|
||||
minWidth: 160,
|
||||
height: 37,
|
||||
borderRadius: 18,
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '14px',
|
||||
letterSpacing: '-1%',
|
||||
});
|
||||
export const learnAIButton = style([
|
||||
purchaseButton,
|
||||
{
|
||||
color: cssVar('textEmphasisColor'),
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
]);
|
||||
export const agreement = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 400,
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
globalStyle(`.${agreement} > a`, {
|
||||
color: cssVar('textPrimaryColor'),
|
||||
textDecoration: 'underline',
|
||||
});
|
||||
|
||||
// benefits
|
||||
export const benefits = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
});
|
||||
export const benefitGroup = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
});
|
||||
export const benefitTitle = style({
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
});
|
||||
globalStyle(`.${benefitTitle} > svg`, {
|
||||
color: cssVar('brandColor'),
|
||||
width: 20,
|
||||
height: 20,
|
||||
});
|
||||
export const benefitList = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
});
|
||||
export const benefitItem = style({
|
||||
fontWeight: 400,
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
lineHeight: '24px',
|
||||
paddingLeft: 22,
|
||||
position: 'relative',
|
||||
'::before': {
|
||||
content: '""',
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
background: 'currentColor',
|
||||
position: 'absolute',
|
||||
left: '10px',
|
||||
top: '10px',
|
||||
},
|
||||
});
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { i18nTime, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { AICancel, AILogin, AIResume, AISubscribe } from './actions';
|
||||
import { AIRedeemCodeButton } from './actions/redeem';
|
||||
import * as styles from './ai-plan.css';
|
||||
import { AIPlanLayout } from './layout';
|
||||
|
||||
export const AIPlan = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const authService = useService(AuthService);
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const subscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const price = useLiveData(subscriptionService.prices.aiPrice$);
|
||||
const isLoggedIn =
|
||||
useLiveData(authService.session.status$) === 'authenticated';
|
||||
const isOnetime = useLiveData(subscriptionService.subscription.isOnetimeAI$);
|
||||
|
||||
useEffect(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
// yearly subscription should always be available
|
||||
if (!price?.yearlyAmount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const billingTip = subscription?.nextBillAt
|
||||
? t['com.affine.payment.ai.billing-tip.next-bill-at']({
|
||||
due: i18nTime(subscription.nextBillAt, {
|
||||
absolute: { accuracy: 'day' },
|
||||
}),
|
||||
})
|
||||
: subscription?.canceledAt && subscription.end
|
||||
? t['com.affine.payment.ai.billing-tip.end-at']({
|
||||
end: i18nTime(subscription.end, {
|
||||
absolute: { accuracy: 'day' },
|
||||
}),
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<AIPlanLayout
|
||||
caption={
|
||||
subscription
|
||||
? t['com.affine.payment.ai.pricing-plan.caption-purchased']()
|
||||
: t['com.affine.payment.ai.pricing-plan.caption-free']()
|
||||
}
|
||||
actionButtons={
|
||||
isLoggedIn ? (
|
||||
subscription ? (
|
||||
isOnetime ? (
|
||||
<AIRedeemCodeButton className={styles.purchaseButton} />
|
||||
) : subscription.canceledAt ? (
|
||||
<AIResume className={styles.purchaseButton} />
|
||||
) : (
|
||||
<AICancel className={styles.purchaseButton} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<AISubscribe
|
||||
className={styles.purchaseButton}
|
||||
displayedFrequency="monthly"
|
||||
/>
|
||||
<a href="https://ai.affine.pro" target="_blank" rel="noreferrer">
|
||||
<Button className={styles.learnAIButton}>
|
||||
{t['com.affine.payment.ai.pricing-plan.learn']()}
|
||||
</Button>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<AILogin className={styles.purchaseButton} />
|
||||
)
|
||||
}
|
||||
billingTip={billingTip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
CheckBoxCheckLinearIcon,
|
||||
PenIcon,
|
||||
TextIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './ai-plan.css';
|
||||
|
||||
const benefitsGetter = (t: ReturnType<typeof useI18n>) => [
|
||||
{
|
||||
name: t['com.affine.payment.ai.benefit.g1'](),
|
||||
icon: <TextIcon />,
|
||||
items: [
|
||||
t['com.affine.payment.ai.benefit.g1-1'](),
|
||||
t['com.affine.payment.ai.benefit.g1-2'](),
|
||||
t['com.affine.payment.ai.benefit.g1-3'](),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t['com.affine.payment.ai.benefit.g2'](),
|
||||
icon: <PenIcon />,
|
||||
items: [
|
||||
t['com.affine.payment.ai.benefit.g2-1'](),
|
||||
t['com.affine.payment.ai.benefit.g2-2'](),
|
||||
t['com.affine.payment.ai.benefit.g2-3'](),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t['com.affine.payment.ai.benefit.g3'](),
|
||||
icon: <CheckBoxCheckLinearIcon />,
|
||||
items: [
|
||||
t['com.affine.payment.ai.benefit.g3-1'](),
|
||||
t['com.affine.payment.ai.benefit.g3-2'](),
|
||||
t['com.affine.payment.ai.benefit.g3-3'](),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const AIBenefits = () => {
|
||||
const t = useI18n();
|
||||
const benefits = useMemo(() => benefitsGetter(t), [t]);
|
||||
return (
|
||||
<div className={styles.benefits}>
|
||||
{benefits.map(({ name, icon, items }) => {
|
||||
return (
|
||||
<div key={name} className={styles.benefitGroup}>
|
||||
<div className={styles.benefitTitle}>
|
||||
{icon}
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<ul className={styles.benefitList}>
|
||||
{items.map(item => (
|
||||
<li className={styles.benefitItem} key={item}>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { PricingCollapsible } from '../layout';
|
||||
import * as styles from './ai-plan.css';
|
||||
import { AIBenefits } from './benefits';
|
||||
|
||||
export interface AIPlanLayoutProps {
|
||||
caption?: ReactNode;
|
||||
actionButtons?: ReactNode;
|
||||
billingTip?: ReactNode;
|
||||
}
|
||||
export const AIPlanLayout = ({
|
||||
caption,
|
||||
actionButtons,
|
||||
billingTip,
|
||||
}: AIPlanLayoutProps) => {
|
||||
const t = useI18n();
|
||||
const title = t['com.affine.payment.ai.pricing-plan.title']();
|
||||
|
||||
return (
|
||||
<PricingCollapsible title={title} caption={caption}>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.titleBlock}>
|
||||
<section className={styles.titleCaption1}>
|
||||
{t['com.affine.payment.ai.pricing-plan.title-caption-1']()}
|
||||
</section>
|
||||
<section className={styles.title}>
|
||||
{t['com.affine.payment.ai.pricing-plan.title']()}
|
||||
</section>
|
||||
<section className={styles.titleCaption2}>
|
||||
{t['com.affine.payment.ai.pricing-plan.title-caption-2']()}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className={styles.actionBlock}>
|
||||
<div className={styles.actionButtons}>{actionButtons}</div>
|
||||
{billingTip ? (
|
||||
<div className={styles.agreement}>{billingTip}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<AIBenefits />
|
||||
</div>
|
||||
</PricingCollapsible>
|
||||
);
|
||||
};
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import type { CreateCheckoutSessionInput } from '@affine/graphql';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
export interface CheckoutSlotProps extends PropsWithChildren {
|
||||
checkoutOptions: Omit<CreateCheckoutSessionInput, 'idempotencyKey'>;
|
||||
onBeforeCheckout?: () => void;
|
||||
onCheckoutError?: (error: any) => void;
|
||||
onCheckoutSuccess?: () => void;
|
||||
renderer: (props: { onClick: () => void; loading: boolean }) => ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for checkout action
|
||||
*/
|
||||
export const CheckoutSlot = ({
|
||||
checkoutOptions,
|
||||
onBeforeCheckout,
|
||||
onCheckoutError,
|
||||
onCheckoutSuccess,
|
||||
renderer: Renderer,
|
||||
}: CheckoutSlotProps) => {
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const [isMutating, setMutating] = useState(false);
|
||||
const [isOpenedExternalWindow, setOpenedExternalWindow] = useState(false);
|
||||
const urlService = useService(UrlService);
|
||||
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
|
||||
useEffect(() => {
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
useEffect(() => {
|
||||
if (isOpenedExternalWindow) {
|
||||
// when the external window is opened, revalidate the subscription when window get focus
|
||||
window.addEventListener(
|
||||
'focus',
|
||||
subscriptionService.subscription.revalidate
|
||||
);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
'focus',
|
||||
subscriptionService.subscription.revalidate
|
||||
);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [isOpenedExternalWindow, subscriptionService]);
|
||||
|
||||
const subscribe = useAsyncCallback(async () => {
|
||||
setMutating(true);
|
||||
onBeforeCheckout?.();
|
||||
try {
|
||||
const session = await subscriptionService.createCheckoutSession({
|
||||
idempotencyKey,
|
||||
...checkoutOptions,
|
||||
});
|
||||
urlService.openPopupWindow(session);
|
||||
setOpenedExternalWindow(true);
|
||||
setIdempotencyKey(nanoid());
|
||||
onCheckoutSuccess?.();
|
||||
} catch (e) {
|
||||
onCheckoutError?.(e);
|
||||
} finally {
|
||||
setMutating(false);
|
||||
}
|
||||
}, [
|
||||
checkoutOptions,
|
||||
idempotencyKey,
|
||||
onBeforeCheckout,
|
||||
onCheckoutError,
|
||||
onCheckoutSuccess,
|
||||
subscriptionService,
|
||||
urlService,
|
||||
]);
|
||||
|
||||
return <Renderer onClick={subscribe} loading={isMutating} />;
|
||||
};
|
||||
-333
@@ -1,333 +0,0 @@
|
||||
import { Switch } from '@affine/component';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { AfFiNeIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { CloudPlanLayout } from './layout';
|
||||
import { LifetimePlan } from './lifetime/lifetime-plan';
|
||||
import { PlanCard } from './plan-card';
|
||||
import { planTitleTitleCaption } from './style.css';
|
||||
import * as styles from './style.css';
|
||||
|
||||
type T = ReturnType<typeof useI18n>;
|
||||
|
||||
export type Benefits = Record<
|
||||
string,
|
||||
Array<{
|
||||
icon?: ReactNode;
|
||||
title: ReactNode;
|
||||
}>
|
||||
>;
|
||||
type BenefitsGetter = (t: T) => Benefits;
|
||||
interface BasePrice {
|
||||
plan: SubscriptionPlan;
|
||||
name: string;
|
||||
description: string;
|
||||
benefits: Benefits;
|
||||
}
|
||||
export interface FixedPrice extends BasePrice {
|
||||
type: 'fixed';
|
||||
price: string;
|
||||
yearlyPrice: string;
|
||||
discount?: string;
|
||||
titleRenderer: (
|
||||
recurring: SubscriptionRecurring,
|
||||
detail: FixedPrice
|
||||
) => ReactNode;
|
||||
}
|
||||
|
||||
export interface DynamicPrice extends BasePrice {
|
||||
type: 'dynamic';
|
||||
contact: boolean;
|
||||
titleRenderer: (
|
||||
recurring: SubscriptionRecurring,
|
||||
detail: DynamicPrice
|
||||
) => ReactNode;
|
||||
}
|
||||
|
||||
const freeBenefits: BenefitsGetter = t => ({
|
||||
[t['com.affine.payment.cloud.free.benefit.g1']()]: ([1, 2, 3] as const).map(
|
||||
i => ({
|
||||
title: t[`com.affine.payment.cloud.free.benefit.g1-${i}`](),
|
||||
})
|
||||
),
|
||||
[t['com.affine.payment.cloud.free.benefit.g2']()]: (
|
||||
[1, 2, 3, 4, 5] as const
|
||||
).map(i => ({
|
||||
title: t[`com.affine.payment.cloud.free.benefit.g2-${i}`](),
|
||||
})),
|
||||
});
|
||||
|
||||
const proBenefits: BenefitsGetter = t => ({
|
||||
[t['com.affine.payment.cloud.pro.benefit.g1']()]: [
|
||||
{
|
||||
title: t['com.affine.payment.cloud.pro.benefit.g1-1'](),
|
||||
icon: <AfFiNeIcon />,
|
||||
},
|
||||
...([2, 3, 4, 5, 7, 8] as const).map(i => ({
|
||||
title: t[`com.affine.payment.cloud.pro.benefit.g1-${i}`](),
|
||||
})),
|
||||
],
|
||||
});
|
||||
|
||||
const teamBenefits: BenefitsGetter = t => ({
|
||||
[t['com.affine.payment.cloud.team.benefit.g1']()]: [
|
||||
{
|
||||
title: t['com.affine.payment.cloud.team.benefit.g1-1'](),
|
||||
icon: <AfFiNeIcon />,
|
||||
},
|
||||
...([2, 3, 4] as const).map(i => ({
|
||||
title: t[`com.affine.payment.cloud.team.benefit.g1-${i}`](),
|
||||
})),
|
||||
],
|
||||
[t['com.affine.payment.cloud.team.benefit.g2']()]: [
|
||||
{ title: t['com.affine.payment.cloud.team.benefit.g2-1']() },
|
||||
{ title: t['com.affine.payment.cloud.team.benefit.g2-2']() },
|
||||
{ title: t['com.affine.payment.cloud.team.benefit.g2-3']() },
|
||||
],
|
||||
});
|
||||
|
||||
export function getPlanDetail(t: T) {
|
||||
return new Map<SubscriptionPlan, FixedPrice | DynamicPrice>([
|
||||
[
|
||||
SubscriptionPlan.Free,
|
||||
{
|
||||
type: 'fixed',
|
||||
plan: SubscriptionPlan.Free,
|
||||
price: '0',
|
||||
yearlyPrice: '0',
|
||||
name: t['com.affine.payment.cloud.free.name'](),
|
||||
description: t['com.affine.payment.cloud.free.description'](),
|
||||
titleRenderer: () => t['com.affine.payment.cloud.free.title'](),
|
||||
benefits: freeBenefits(t),
|
||||
},
|
||||
],
|
||||
[
|
||||
SubscriptionPlan.Pro,
|
||||
{
|
||||
type: 'fixed',
|
||||
plan: SubscriptionPlan.Pro,
|
||||
price: '1',
|
||||
yearlyPrice: '1',
|
||||
name: t['com.affine.payment.cloud.pro.name'](),
|
||||
description: t['com.affine.payment.cloud.pro.description'](),
|
||||
titleRenderer: (recurring, detail) => {
|
||||
const price =
|
||||
recurring === SubscriptionRecurring.Yearly
|
||||
? detail.yearlyPrice
|
||||
: detail.price;
|
||||
return (
|
||||
<>
|
||||
{t['com.affine.payment.cloud.pro.title.price-monthly']({
|
||||
price: '$' + price,
|
||||
})}
|
||||
{recurring === SubscriptionRecurring.Yearly ? (
|
||||
<span className={planTitleTitleCaption}>
|
||||
{t['com.affine.payment.cloud.pro.title.billed-yearly']()}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
},
|
||||
benefits: proBenefits(t),
|
||||
},
|
||||
],
|
||||
[
|
||||
SubscriptionPlan.Team,
|
||||
{
|
||||
type: 'dynamic',
|
||||
plan: SubscriptionPlan.Team,
|
||||
contact: true,
|
||||
name: t['com.affine.payment.cloud.team.name'](),
|
||||
description: t['com.affine.payment.cloud.team.description'](),
|
||||
titleRenderer: () => t['com.affine.payment.cloud.team.title'](),
|
||||
benefits: teamBenefits(t),
|
||||
},
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
const getRecurringLabel = ({
|
||||
recurring,
|
||||
t,
|
||||
}: {
|
||||
recurring: SubscriptionRecurring;
|
||||
t: ReturnType<typeof useI18n>;
|
||||
}) => {
|
||||
return recurring === SubscriptionRecurring.Monthly
|
||||
? t['com.affine.payment.recurring-monthly']()
|
||||
: t['com.affine.payment.recurring-yearly']();
|
||||
};
|
||||
|
||||
export const CloudPlans = () => {
|
||||
const t = useI18n();
|
||||
const scrollWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { authService, subscriptionService } = useServices({
|
||||
AuthService,
|
||||
SubscriptionService,
|
||||
});
|
||||
|
||||
const prices = useLiveData(subscriptionService.prices.prices$);
|
||||
const loggedIn = useLiveData(authService.session.status$) === 'authenticated';
|
||||
const proSubscription = useLiveData(subscriptionService.subscription.pro$);
|
||||
const isOnetimePro = useLiveData(
|
||||
subscriptionService.subscription.isOnetimePro$
|
||||
);
|
||||
|
||||
const [recurring, setRecurring] = useState<SubscriptionRecurring>(
|
||||
proSubscription?.recurring ?? SubscriptionRecurring.Yearly
|
||||
);
|
||||
|
||||
const planDetail = useMemo(() => {
|
||||
const rawMap = getPlanDetail(t);
|
||||
const clonedMap = new Map<SubscriptionPlan, FixedPrice | DynamicPrice>();
|
||||
|
||||
rawMap.forEach((detail, plan) => {
|
||||
clonedMap.set(plan, { ...detail });
|
||||
});
|
||||
|
||||
prices?.forEach(price => {
|
||||
const detail = clonedMap.get(price.plan);
|
||||
|
||||
if (detail?.type === 'fixed') {
|
||||
detail.price = ((price.amount ?? 0) / 100).toFixed(2);
|
||||
detail.yearlyPrice = ((price.yearlyAmount ?? 0) / 100 / 12).toFixed(2);
|
||||
detail.discount =
|
||||
price.yearlyAmount && price.amount
|
||||
? Math.floor(
|
||||
(1 - price.yearlyAmount / 12 / price.amount) * 100
|
||||
).toString()
|
||||
: undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return clonedMap;
|
||||
}, [prices, t]);
|
||||
|
||||
const currentPlan = proSubscription?.plan ?? SubscriptionPlan.Free;
|
||||
const isCanceled = !!proSubscription?.canceledAt;
|
||||
const currentRecurring =
|
||||
proSubscription?.recurring ?? SubscriptionRecurring.Monthly;
|
||||
const yearlyDiscount = (
|
||||
planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined
|
||||
)?.discount;
|
||||
|
||||
// auto scroll to current plan card
|
||||
useEffect(() => {
|
||||
if (!scrollWrapper.current) return;
|
||||
const currentPlanCard = scrollWrapper.current?.querySelector(
|
||||
'[data-current="true"]'
|
||||
);
|
||||
const wrapperComputedStyle = getComputedStyle(scrollWrapper.current);
|
||||
const left = currentPlanCard
|
||||
? currentPlanCard.getBoundingClientRect().left -
|
||||
scrollWrapper.current.getBoundingClientRect().left -
|
||||
parseInt(wrapperComputedStyle.paddingLeft)
|
||||
: 0;
|
||||
const appeared = scrollWrapper.current.dataset.appeared === 'true';
|
||||
const animationFrameId = requestAnimationFrame(() => {
|
||||
scrollWrapper.current?.scrollTo({
|
||||
behavior: appeared ? 'smooth' : 'instant',
|
||||
left,
|
||||
});
|
||||
scrollWrapper.current?.setAttribute('data-appeared', 'true');
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, [recurring]);
|
||||
|
||||
// caption
|
||||
const cloudCaption = loggedIn ? (
|
||||
isCanceled ? (
|
||||
<p>
|
||||
{t['com.affine.payment.subtitle-canceled']({
|
||||
plan: `${getRecurringLabel({
|
||||
recurring: currentRecurring,
|
||||
t,
|
||||
})} ${currentPlan}`,
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<Trans
|
||||
plan={currentPlan}
|
||||
i18nKey="com.affine.payment.subtitle-active"
|
||||
values={{ currentPlan }}
|
||||
>
|
||||
You are currently on the {{ currentPlan }} plan. If you have any
|
||||
questions, please contact our
|
||||
<a
|
||||
href="mailto:support@toeverything.info"
|
||||
style={{ color: 'var(--affine-link-color)' }}
|
||||
>
|
||||
customer support
|
||||
</a>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<p>{t['com.affine.payment.subtitle-not-signed-in']()}</p>
|
||||
);
|
||||
|
||||
// toggle
|
||||
const cloudToggle = (
|
||||
<div className={styles.recurringToggleWrapper}>
|
||||
<div>
|
||||
<div className={styles.recurringToggleRecurring}>
|
||||
<span>
|
||||
{t['com.affine.payment.cloud.pricing-plan.toggle-billed-yearly']()}
|
||||
</span>
|
||||
</div>
|
||||
{yearlyDiscount ? (
|
||||
<div className={styles.recurringToggleDiscount}>
|
||||
{t['com.affine.payment.cloud.pricing-plan.toggle-discount']({
|
||||
discount: yearlyDiscount,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Switch
|
||||
checked={recurring === SubscriptionRecurring.Yearly}
|
||||
onChange={checked =>
|
||||
setRecurring(
|
||||
checked
|
||||
? SubscriptionRecurring.Yearly
|
||||
: SubscriptionRecurring.Monthly
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const cloudScroll = (
|
||||
<div className={styles.planCardsWrapper} ref={scrollWrapper}>
|
||||
{Array.from(planDetail.values()).map(detail => {
|
||||
return <PlanCard key={detail.plan} {...{ detail, recurring }} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const cloudSelect = (
|
||||
<div className={styles.cloudSelect}>
|
||||
<b>{t['com.affine.payment.cloud.pricing-plan.select.title']()}</b>
|
||||
<span>{t['com.affine.payment.cloud.pricing-plan.select.caption']()}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<CloudPlanLayout
|
||||
caption={cloudCaption}
|
||||
select={cloudSelect}
|
||||
toggle={cloudToggle}
|
||||
scroll={cloudScroll}
|
||||
scrollRef={scrollWrapper}
|
||||
lifetime={isOnetimePro ? null : <LifetimePlan />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
export function BulledListIcon({ color = 'currentColor' }: { color: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<ellipse
|
||||
cx="4.00008"
|
||||
cy="7.99984"
|
||||
rx="1.33333"
|
||||
ry="1.33333"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
|
||||
import { SWRErrorBoundary } from '../../../../../components/pure/swr-error-bundary';
|
||||
import { SubscriptionService } from '../../../../../modules/cloud';
|
||||
import { AIPlan } from './ai/ai-plan';
|
||||
import { CloudPlans } from './cloud-plans';
|
||||
import { CloudPlanLayout, PlanLayout } from './layout';
|
||||
import { PlansSkeleton } from './skeleton';
|
||||
import * as styles from './style.css';
|
||||
|
||||
const Settings = () => {
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const prices = useLiveData(subscriptionService.prices.prices$);
|
||||
|
||||
useEffect(() => {
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
|
||||
if (prices === null) {
|
||||
return <PlansSkeleton />;
|
||||
}
|
||||
|
||||
return <PlanLayout cloudTip cloud={<CloudPlans />} ai={<AIPlan />} />;
|
||||
};
|
||||
|
||||
export const AFFiNEPricingPlans = () => {
|
||||
return (
|
||||
<SWRErrorBoundary FallbackComponent={PlansErrorBoundary}>
|
||||
<Settings />
|
||||
</SWRErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const PlansErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const scroll = (
|
||||
<div className={styles.errorTip}>
|
||||
<span>{t['com.affine.payment.plans-error-tip']()}</span>
|
||||
<a onClick={resetErrorBoundary} className={styles.errorTipRetry}>
|
||||
{t['com.affine.payment.plans-error-retry']()}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
return <PlanLayout cloud={<CloudPlanLayout scroll={scroll} />} />;
|
||||
};
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, keyframes, style } from '@vanilla-extract/css';
|
||||
export const plansLayoutRoot = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '24px',
|
||||
});
|
||||
export const scrollArea = style({
|
||||
marginLeft: 'calc(-1 * var(--setting-modal-gap-x))',
|
||||
paddingLeft: 'var(--setting-modal-gap-x)',
|
||||
width: 'var(--setting-modal-width)',
|
||||
overflowX: 'auto',
|
||||
// scrollSnapType: 'x mandatory',
|
||||
paddingBottom: '21px',
|
||||
/** Avoid box-shadow clipping */
|
||||
paddingTop: '21px',
|
||||
marginTop: '-21px',
|
||||
});
|
||||
export const scrollBar = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
touchAction: 'none',
|
||||
height: '9px',
|
||||
width: '100%',
|
||||
});
|
||||
export const scrollThumb = style({
|
||||
background: cssVar('iconSecondary'),
|
||||
opacity: 0.6,
|
||||
overflow: 'hidden',
|
||||
height: '4px',
|
||||
borderRadius: '4px',
|
||||
vars: {
|
||||
'--radix-scroll-area-thumb-height': '4px',
|
||||
},
|
||||
});
|
||||
export const allPlansLink = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
color: cssVar('linkColor'),
|
||||
background: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
|
||||
export const collapsibleHeader = style({
|
||||
display: 'flex',
|
||||
alignItems: 'start',
|
||||
marginBottom: 8,
|
||||
});
|
||||
export const collapsibleHeaderContent = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
});
|
||||
export const collapsibleHeaderTitle = style({
|
||||
fontWeight: 600,
|
||||
fontSize: cssVar('fontBase'),
|
||||
lineHeight: '22px',
|
||||
});
|
||||
export const collapsibleHeaderCaption = style({
|
||||
fontWeight: 400,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
|
||||
export const affineCloudHeader = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
});
|
||||
export const aiDivider = style({
|
||||
opacity: 0,
|
||||
selectors: {
|
||||
'[data-cloud-visible] &': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const slideInBottom = keyframes({
|
||||
from: {
|
||||
marginBottom: -100,
|
||||
},
|
||||
to: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
});
|
||||
export const aiScrollTip = style({
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
bottom: 12,
|
||||
width: 'var(--setting-modal-content-width)',
|
||||
background: cssVar('white'),
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
transition: 'transform 0.36s ease 0.4s, opacity 0.3s ease 0.46s',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 20px 12px 16px',
|
||||
boxShadow: cssVar('shadow1'),
|
||||
marginBottom: -100,
|
||||
|
||||
animation: `${slideInBottom} 0.3s ease 0.5s forwards`,
|
||||
|
||||
selectors: {
|
||||
'[data-cloud-visible] &': {
|
||||
transform: 'translateY(100px)',
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
// to override `display: contents !important` in `scrollable.tsx`
|
||||
globalStyle(`div.${aiScrollTip}`, {
|
||||
display: 'flex !important',
|
||||
});
|
||||
export const cloudScrollTipTitle = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
});
|
||||
export const cloudScrollTipCaption = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 400,
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
import { Button, Divider, IconButton } from '@affine/component';
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import {
|
||||
openSettingModalAtom,
|
||||
type PlansScrollAnchor,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ArrowRightBigIcon, ArrowUpSmallIcon } from '@blocksuite/icons/rc';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import * as ScrollArea from '@radix-ui/react-scroll-area';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import {
|
||||
type HtmlHTMLAttributes,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal, flushSync } from 'react-dom';
|
||||
|
||||
import { settingModalScrollContainerAtom } from '../../atoms';
|
||||
import * as styles from './layout.css';
|
||||
|
||||
export const SeeAllLink = () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<a
|
||||
className={styles.allPlansLink}
|
||||
href="https://affine.pro/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t['com.affine.payment.see-all-plans']()}
|
||||
{<ArrowRightBigIcon width="16" height="16" />}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
interface PricingCollapsibleProps
|
||||
extends Omit<HtmlHTMLAttributes<HTMLDivElement>, 'title'> {
|
||||
title?: ReactNode;
|
||||
caption?: ReactNode;
|
||||
}
|
||||
export const PricingCollapsible = ({
|
||||
title,
|
||||
caption,
|
||||
children,
|
||||
}: PricingCollapsibleProps) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const toggle = useCallback(() => setOpen(prev => !prev), []);
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<section className={styles.collapsibleHeader}>
|
||||
<div className={styles.collapsibleHeaderContent}>
|
||||
<div className={styles.collapsibleHeaderTitle}>{title}</div>
|
||||
<div className={styles.collapsibleHeaderCaption}>{caption}</div>
|
||||
</div>
|
||||
<IconButton onClick={toggle} size="20">
|
||||
<ArrowUpSmallIcon
|
||||
style={{
|
||||
transform: open ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
transition: 'transform 0.23s ease',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</section>
|
||||
<Collapsible.Content>{children}</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PlanLayoutProps {
|
||||
cloud?: ReactNode;
|
||||
ai?: ReactNode;
|
||||
cloudTip?: boolean;
|
||||
}
|
||||
|
||||
export const PlanLayout = ({ cloud, ai, cloudTip }: PlanLayoutProps) => {
|
||||
const t = useI18n();
|
||||
const [modal, setOpenSettingModal] = useAtom(openSettingModalAtom);
|
||||
const scrollAnchor = modal.activeTab === 'plans' ? modal.scrollAnchor : null;
|
||||
const plansRootRef = useRef<HTMLDivElement>(null);
|
||||
const cloudScrollTipRef = useRef<HTMLDivElement>(null);
|
||||
const settingModalScrollContainer = useAtomValue(
|
||||
settingModalScrollContainerAtom
|
||||
);
|
||||
|
||||
const updateCloudTipState = useCallback(() => {
|
||||
if (!cloudTip) return;
|
||||
const cloudContainer =
|
||||
plansRootRef.current?.querySelector('#cloudPricingPlan');
|
||||
if (!settingModalScrollContainer || !cloudContainer) return;
|
||||
|
||||
const minVisibleHeight = 30;
|
||||
|
||||
const containerRect = settingModalScrollContainer.getBoundingClientRect();
|
||||
const cloudTop =
|
||||
cloudContainer.getBoundingClientRect().top - containerRect.top;
|
||||
const cloudIntoView = cloudTop < containerRect.height - minVisibleHeight;
|
||||
if (cloudIntoView) {
|
||||
settingModalScrollContainer.dataset.cloudVisible = '';
|
||||
}
|
||||
}, [cloudTip, settingModalScrollContainer]);
|
||||
|
||||
// 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();
|
||||
setOpenSettingModal(prev => ({ ...prev, scrollAnchor: undefined }));
|
||||
}
|
||||
});
|
||||
}, [scrollAnchor, setOpenSettingModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingModalScrollContainer || !cloudScrollTipRef.current) return;
|
||||
|
||||
settingModalScrollContainer.addEventListener('scroll', updateCloudTipState);
|
||||
updateCloudTipState();
|
||||
return () => {
|
||||
settingModalScrollContainer.removeEventListener(
|
||||
'scroll',
|
||||
updateCloudTipState
|
||||
);
|
||||
};
|
||||
}, [settingModalScrollContainer, updateCloudTipState]);
|
||||
|
||||
const scrollToAnchor = useCallback((anchor: PlansScrollAnchor) => {
|
||||
const target = plansRootRef.current?.querySelector(`#${anchor}`);
|
||||
target && target.scrollIntoView({ behavior: 'smooth' });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.plansLayoutRoot} ref={plansRootRef}>
|
||||
{/* TODO(@catsjuice): SettingHeader component shouldn't have margin itself */}
|
||||
<SettingHeader
|
||||
style={{ marginBottom: '0px' }}
|
||||
title={t['com.affine.payment.title']()}
|
||||
/>
|
||||
{ai ? (
|
||||
<>
|
||||
<div id="aiPricingPlan">{ai}</div>
|
||||
<Divider className={styles.aiDivider} />
|
||||
</>
|
||||
) : null}
|
||||
<div id="cloudPricingPlan">{cloud}</div>
|
||||
|
||||
{cloudTip && settingModalScrollContainer
|
||||
? createPortal(
|
||||
<div className={styles.aiScrollTip} ref={cloudScrollTipRef}>
|
||||
<div>
|
||||
<div className={styles.cloudScrollTipTitle}>
|
||||
{t['com.affine.cloud-scroll-tip.title']()}
|
||||
</div>
|
||||
<div className={styles.cloudScrollTipCaption}>
|
||||
{t['com.affine.cloud-scroll-tip.caption']()}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => scrollToAnchor('cloudPricingPlan')}
|
||||
variant="primary"
|
||||
>
|
||||
{t['com.affine.ai-scroll-tip.view']()}
|
||||
</Button>
|
||||
</div>,
|
||||
settingModalScrollContainer,
|
||||
'aiScrollTip'
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PlanCardProps {
|
||||
title?: ReactNode;
|
||||
caption?: ReactNode;
|
||||
select?: ReactNode;
|
||||
toggle?: ReactNode;
|
||||
scroll?: ReactNode;
|
||||
lifetime?: ReactNode;
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
export const CloudPlanLayout = ({
|
||||
title = 'AFFiNE Cloud',
|
||||
caption,
|
||||
select,
|
||||
toggle,
|
||||
scroll,
|
||||
lifetime,
|
||||
scrollRef,
|
||||
}: PlanCardProps) => {
|
||||
return (
|
||||
<PricingCollapsible title={title} caption={caption}>
|
||||
<div className={styles.affineCloudHeader}>
|
||||
<div>{select}</div>
|
||||
<div>{toggle}</div>
|
||||
</div>
|
||||
<ScrollArea.Root>
|
||||
<ScrollArea.Viewport ref={scrollRef} className={styles.scrollArea}>
|
||||
{scroll}
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
forceMount
|
||||
orientation="horizontal"
|
||||
className={styles.scrollBar}
|
||||
>
|
||||
<ScrollArea.Thumb className={styles.scrollThumb}></ScrollArea.Thumb>
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
{lifetime ? (
|
||||
<div style={{ paddingTop: 12 }} id="lifetimePricingPlan">
|
||||
{lifetime}
|
||||
</div>
|
||||
) : null}
|
||||
</PricingCollapsible>
|
||||
);
|
||||
};
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
import {
|
||||
affineIconGradientStart,
|
||||
affineIconGradientStop,
|
||||
} from './believer-card.css';
|
||||
|
||||
export const bgAFFiNERaw = `<svg
|
||||
width="352"
|
||||
height="320"
|
||||
viewBox="0 0 352 320"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="affine-svg"
|
||||
>
|
||||
<g filter="url(#filter0_dddd_782_4147)">
|
||||
<path
|
||||
fill="url(#believerAffineIconGradient)"
|
||||
class="affine"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M116.696 47.3266C123.09 36.2513 127.89 27.9391 130.138 24.0455L130.131 24.0532C131.853 21.0201 136.347 21.0584 138.07 24.0532C138.899 25.4399 140.482 28.2114 142.109 31.0592L142.112 31.0646L142.113 31.0662C143.926 34.2404 145.793 37.5081 146.729 39.0578C159.768 61.6638 175.427 88.7902 191.338 116.352L191.353 116.378C198.777 129.239 206.256 142.194 213.548 154.829L116.696 47.3266ZM95.6843 83.7222C65.9312 135.266 27.4975 201.875 21.1035 213.078C19.9705 215.804 22.0605 219.243 25.031 219.297C25.6193 219.354 29.0911 219.347 31.3338 219.343H31.3345H31.3351H31.3356H31.3361C32.1278 219.342 32.766 219.341 33.0697 219.343C38.9111 219.345 45.1459 219.347 51.6934 219.348L95.6843 83.7222ZM94.8237 219.35C111.168 219.349 128.22 219.347 145.141 219.345C180.097 219.34 214.49 219.336 240.911 219.343L241.212 219.343H241.213C242.07 219.344 243.123 219.345 243.177 219.297C243.387 219.315 243.57 219.273 243.751 219.232C243.88 219.202 244.008 219.173 244.142 219.167C246.615 218.424 248.108 215.513 247.105 213.085C247.007 212.944 246.967 212.852 246.908 212.721C246.824 212.532 246.703 212.26 246.324 211.638C242.34 204.727 238.108 197.391 233.697 189.745L94.8237 219.35ZM144.914 11.4689C136.24 5.09634 123.501 7.80009 118.203 17.1521C113.965 24.553 101.087 46.8414 85.5332 73.7598C55.3395 126.015 15.0642 195.719 8.34886 207.831C4.21469 217.757 9.97191 229.53 20.3226 232.417C23.369 233.225 26.5304 233.17 29.5054 233.117C30.7322 233.095 31.9274 233.074 33.0697 233.114C60.2291 233.14 95.7969 233.126 131.767 233.111C171.174 233.095 211.065 233.078 240.911 233.114C242.565 233.145 245.451 233.114 247.886 232.425C255.396 230.334 260.977 223.188 261.199 215.398C261.284 212.817 260.801 210.236 259.867 207.846C259.015 205.998 258.311 204.797 257.619 203.617L257.619 203.617C257.354 203.164 257.09 202.714 256.82 202.232L251.048 192.229C238.163 169.9 221.518 141.059 204.873 112.217L204.167 110.993C187.749 82.5434 171.382 54.1817 158.672 32.1567L152.899 22.1537L150.013 17.1521C148.704 14.9845 146.989 13.0084 144.914 11.4689ZM215.983 167.938C213.319 165.616 210.582 163.395 207.767 161.299L113.985 206.625C113.754 206.788 113.522 206.95 113.289 207.111L226.643 179.013L215.983 167.938ZM134.763 187.748L196.966 154.14C193.36 152.042 189.636 150.163 185.788 148.543L134.763 187.748ZM144.168 170.768L174.282 144.611C172.663 144.183 171.023 143.802 169.362 143.469C166.442 142.936 163.506 142.562 160.552 142.374L144.168 170.768ZM129.17 179.638C127.529 182.104 125.738 184.462 123.815 186.722C122.704 187.988 121.561 189.211 120.388 190.394L112.8 151.268L129.17 179.638ZM112.596 133.826C111.282 131.172 110.136 128.44 109.139 125.639C108.601 124.047 108.115 122.449 107.679 120.846L145.334 133.826H112.596ZM105.342 83.7389L190.087 141.396C190.209 141.45 190.33 141.504 190.452 141.558C190.535 141.596 190.619 141.633 190.702 141.671L110.653 58.5003L108.898 64.5935C107.307 70.9253 106.076 77.3241 105.342 83.7389ZM88.0881 211.55C80.8724 214.586 73.3244 216.987 65.645 218.921L64.3555 219.241L95.4864 111.157L88.0881 211.55ZM111.196 198.43C107.536 201.207 103.681 203.696 99.6707 205.931L101.828 129.939C102 130.403 102.176 130.866 102.356 131.328L111.196 198.43ZM163.608 132.998L104.577 96.6275C104.573 100.712 104.808 104.79 105.317 108.847L163.608 132.998ZM119.438 145.959L131.963 167.657H131.97C132.927 169.312 135.308 169.312 136.265 167.657L148.79 145.959C149.74 144.304 148.545 142.236 146.639 142.236H121.589C119.675 142.236 118.481 144.304 119.438 145.959Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="believerAffineIconGradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="352"
|
||||
y2="320"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="${affineIconGradientStart}"/>
|
||||
<stop offset="1" stop-color="${affineIconGradientStop}"/>
|
||||
</linearGradient>
|
||||
<filter id="filter0_dddd_782_4147" x="0" y="0" width="351.209" height="319.144" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="4" dy="3"/>
|
||||
<feGaussianBlur stdDeviation="5.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.34902 0 0 0 0 0.34902 0 0 0 0 0.34902 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_782_4147"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="14" dy="14"/>
|
||||
<feGaussianBlur stdDeviation="10"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.34902 0 0 0 0 0.34902 0 0 0 0 0.34902 0 0 0 0.09 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_782_4147" result="effect2_dropShadow_782_4147"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="32" dy="30"/>
|
||||
<feGaussianBlur stdDeviation="13.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.34902 0 0 0 0 0.34902 0 0 0 0 0.34902 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_782_4147" result="effect3_dropShadow_782_4147"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="58" dy="54"/>
|
||||
<feGaussianBlur stdDeviation="16"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.34902 0 0 0 0 0.34902 0 0 0 0 0.34902 0 0 0 0.01 0"/>
|
||||
<feBlend mode="normal" in2="effect3_dropShadow_782_4147" result="effect4_dropShadow_782_4147"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_782_4147" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const bgIconsRaw = `<svg
|
||||
class="icons-svg"
|
||||
width="167"
|
||||
height="174"
|
||||
viewBox="0 0 167 174"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g class="icons">
|
||||
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M148.989 6.74753C145.221 5.58339 141.223 7.69404 140.059 11.4618C139.9 11.9777 139.802 12.4964 139.761 13.0105C139.706 13.704 139.099 14.2216 138.405 14.1667C135.915 13.9694 133.532 15.5142 132.764 17.9999C131.873 20.8811 133.487 23.9385 136.369 24.8287L150.816 29.2925C154.583 30.4566 158.581 28.3459 159.746 24.5782C160.91 20.8104 158.799 16.8123 155.031 15.6482C154.986 15.6341 154.94 15.6205 154.894 15.6073C154.309 15.439 153.928 14.8767 153.989 14.2711C154.319 10.9618 152.289 7.76701 148.989 6.74753ZM137.651 10.7178C139.226 5.62029 144.636 2.76471 149.733 4.33972C153.864 5.61605 156.521 9.40853 156.544 13.5133C161.155 15.3711 163.647 20.4868 162.153 25.3221C160.578 30.4197 155.169 33.2753 150.072 31.7003L135.625 27.2365C131.414 25.9354 129.055 21.467 130.356 17.256C131.356 14.0189 134.226 11.878 137.413 11.6499C137.476 11.3384 137.556 11.0274 137.651 10.7178Z" />
|
||||
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M123.676 148.269L128.617 149.932C129.889 146.577 131.619 143.709 133.495 141.716C133.878 141.31 134.28 140.927 134.697 140.58C129.985 140.892 125.67 143.774 123.676 148.269ZM140.477 138.924C132.398 136.205 123.644 140.549 120.924 148.628C118.205 156.707 122.549 165.46 130.628 168.18C138.706 170.9 147.46 166.555 150.18 158.477C152.9 150.398 148.555 141.644 140.477 138.924ZM139.699 141.234C138.714 140.903 137.144 141.396 135.269 143.387C133.669 145.087 132.106 147.632 130.929 150.71L140.953 154.085C141.877 150.921 142.172 147.949 141.925 145.628C141.636 142.909 140.684 141.566 139.699 141.234ZM143.264 154.863C144.281 151.422 144.638 148.091 144.348 145.37C144.289 144.815 144.2 144.267 144.078 143.738C147.642 146.836 149.336 151.741 148.206 156.526L143.264 154.863ZM140.175 156.394L130.151 153.02C129.227 156.183 128.932 159.155 129.179 161.477C129.469 164.196 130.42 165.539 131.405 165.871C132.39 166.202 133.96 165.708 135.835 163.717C137.436 162.018 138.998 159.473 140.175 156.394ZM136.407 166.525C136.824 166.177 137.226 165.795 137.609 165.388C139.485 163.396 141.215 160.528 142.487 157.173L147.428 158.836C145.435 163.331 141.119 166.213 136.407 166.525ZM127.026 163.367C126.904 162.838 126.815 162.29 126.756 161.734C126.467 159.013 126.823 155.683 127.84 152.242L122.899 150.578C121.768 155.364 123.462 160.269 127.026 163.367Z" />
|
||||
<g class="star">
|
||||
<path fill="currentColor" d="M16.4781 27.0219C16.2651 26.4531 15.6593 26.1346 15.07 26.2816C14.4807 26.4286 14.0955 26.9944 14.1746 27.5965C14.6893 31.5122 14.222 34.4734 12.8463 36.769C11.4706 39.0645 9.07964 40.8729 5.38392 42.2654C4.81558 42.4796 4.49833 43.0861 4.64655 43.675C4.79477 44.264 5.36129 44.6481 5.9633 44.5677C9.81233 44.0538 12.8303 44.5064 15.1718 45.8808C17.5046 47.25 19.3365 49.6344 20.6191 53.3293C20.8222 53.9144 21.4388 54.2473 22.0395 54.0961C22.6401 53.9449 23.0256 53.3597 22.9275 52.7481C22.3179 48.9507 22.7939 45.9282 24.2039 43.5753C25.6139 41.2225 28.0549 39.3775 31.6911 38.1244C32.2767 37.9226 32.6109 37.3068 32.4611 36.7058C32.3112 36.1048 31.7269 35.718 31.1151 35.8148C27.252 36.426 24.2855 35.9348 21.9781 34.5233C19.662 33.1064 17.8399 30.6585 16.4781 27.0219Z" />
|
||||
<path fill="currentColor" d="M27.5899 18.6428C27.5071 18.4216 27.2715 18.2978 27.0423 18.355C26.8132 18.4121 26.6634 18.6321 26.6941 18.8663C26.8943 20.3891 26.7125 21.5407 26.1776 22.4334C25.6426 23.3261 24.7128 24.0293 23.2755 24.5709C23.0545 24.6542 22.9311 24.89 22.9888 25.1191C23.0464 25.3481 23.2667 25.4975 23.5008 25.4662C24.9977 25.2664 26.1713 25.4424 27.0819 25.9769C27.9891 26.5093 28.7015 27.4366 29.2003 28.8735C29.2793 29.101 29.5191 29.2305 29.7527 29.1717C29.9863 29.1129 30.1362 28.8853 30.098 28.6475C29.861 27.1707 30.0461 25.9953 30.5944 25.0803C31.1427 24.1653 32.092 23.4478 33.5061 22.9605C33.7338 22.882 33.8638 22.6425 33.8055 22.4088C33.7472 22.1751 33.52 22.0247 33.2821 22.0623C31.7798 22.3 30.6261 22.109 29.7288 21.56C28.8281 21.009 28.1195 20.0571 27.5899 18.6428Z" />
|
||||
</g>
|
||||
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M39.763 123.789C37.1323 124.113 35.508 126.75 36.1349 129.679L38.9392 142.784C39.5661 145.713 42.2069 147.826 44.8375 147.502L47.2471 147.206L48.676 153.883C48.7894 154.413 49.1784 154.855 49.6616 155.004C50.1447 155.153 50.6269 154.979 50.8832 154.562L56.0842 146.119L62.7695 145.298C65.4002 144.974 67.0246 142.337 66.3977 139.408L63.5933 126.304C62.9664 123.374 60.3257 121.262 57.695 121.585L39.763 123.789ZM38.4884 129.39C38.1713 127.908 38.993 126.574 40.3238 126.41L58.2559 124.206C59.5867 124.042 60.9226 125.111 61.2397 126.593L64.0441 139.697C64.3612 141.179 63.5395 142.513 62.2087 142.677L55.0359 143.559C54.7238 143.597 54.454 143.772 54.2859 144.045L50.3526 150.43L49.3203 145.606C49.1654 144.882 48.513 144.36 47.863 144.44L44.2766 144.881C42.9458 145.045 41.6099 143.976 41.2928 142.494L38.4884 129.39ZM46.0992 137.8C46.9663 137.693 47.5013 136.822 47.2942 135.855C47.0872 134.888 46.2165 134.191 45.3494 134.298C44.4824 134.405 43.9474 135.275 44.1544 136.242C44.3615 137.21 45.2322 137.907 46.0992 137.8ZM52.9661 134.72C53.1732 135.687 52.6381 136.558 51.7711 136.665C50.9041 136.772 50.0334 136.075 49.8263 135.108C49.6193 134.141 50.1543 133.27 51.0213 133.163C51.8884 133.056 52.7591 133.753 52.9661 134.72ZM57.443 135.53C58.31 135.423 58.8451 134.552 58.638 133.585C58.431 132.618 57.5602 131.921 56.6932 132.028C55.8262 132.135 55.2912 133.006 55.4982 133.973C55.7053 134.94 56.576 135.637 57.443 135.53Z" />
|
||||
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M93.966 64.4554L93.9641 64.4567L92.0333 65.8359L94.2622 66.0913C94.8579 66.1595 95.2854 66.6977 95.2172 67.2933C95.1489 67.889 94.6108 68.3165 94.0151 68.2483L88.9821 67.6717C88.3864 67.6035 87.9589 67.0653 88.0271 66.4697L88.5945 61.5165C88.6628 60.9209 89.2009 60.4933 89.7966 60.5616C90.3922 60.6298 90.8198 61.168 90.7515 61.7636L90.4621 64.2901L92.7097 62.6846C95.1632 60.9478 98.2435 60.0723 101.465 60.4414C108.415 61.2375 113.403 67.5162 112.607 74.4654C111.811 81.4146 105.532 86.4027 98.5826 85.6066C92.5931 84.9205 88.0626 80.1627 87.433 74.4134C87.3678 73.8175 87.798 73.2814 88.394 73.2162C88.9899 73.1509 89.526 73.5811 89.5912 74.1771C90.1129 78.9404 93.8694 82.8814 98.8297 83.4496C104.588 84.1092 109.79 79.9762 110.45 74.2183C111.109 68.4604 106.976 63.258 101.218 62.5984C98.5483 62.2926 96.0001 63.0161 93.966 64.4554Z" />
|
||||
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M100.889 65.4744C101.484 65.5427 101.912 66.0809 101.844 66.6765L101.154 72.7008L104.694 77.157C105.067 77.6264 104.989 78.3093 104.519 78.6822C104.05 79.0551 103.367 78.9769 102.994 78.5075L99.2113 73.7462C99.0083 73.4907 98.9152 73.1651 98.9523 72.8409L99.6868 66.4294C99.755 65.8338 100.293 65.4062 100.889 65.4744Z" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const affineIconGradientStart = createVar();
|
||||
export const affineIconGradientStop = createVar();
|
||||
|
||||
const colorSchemes = {
|
||||
light: {
|
||||
dot: '#E0E0E0',
|
||||
affine: {
|
||||
start: '#fff',
|
||||
stop: '#fff',
|
||||
},
|
||||
icon: 'rgba(0,0,0,0.1)',
|
||||
},
|
||||
dark: {
|
||||
dot: 'rgba(255,255,255,0.1)',
|
||||
affine: {
|
||||
start: '#8C8C8C',
|
||||
stop: '#262626',
|
||||
},
|
||||
icon: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
export const card = style({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
minHeight: 200,
|
||||
borderRadius: 16,
|
||||
padding: '20px 24px',
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
overflow: 'hidden',
|
||||
background: cssVar('white'),
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
position: 'relative',
|
||||
zIndex: 3,
|
||||
});
|
||||
|
||||
export const bg = style({
|
||||
vars: {
|
||||
'--dot': colorSchemes.light.dot,
|
||||
},
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundImage:
|
||||
'radial-gradient(circle, var(--dot) 1.2px, transparent 1.2px)',
|
||||
backgroundSize: '12px 12px',
|
||||
backgroundRepeat: 'repeat',
|
||||
|
||||
selectors: {
|
||||
'[data-theme="dark"] &': {
|
||||
vars: {
|
||||
'--dot': colorSchemes.dark.dot,
|
||||
},
|
||||
},
|
||||
|
||||
[`${card}[data-type="1"] &::after`]: {
|
||||
background: `linear-gradient(231deg, transparent 0%, ${cssVar('white')} 80%)`,
|
||||
},
|
||||
[`${card}[data-type="2"] &::after`]: {
|
||||
background: `linear-gradient(290deg, transparent 0%, ${cssVar('white')} 40%)`,
|
||||
},
|
||||
},
|
||||
|
||||
// Overlay
|
||||
'::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
});
|
||||
globalStyle(`.${bg} > svg.affine-svg`, {
|
||||
vars: {
|
||||
[affineIconGradientStart]: colorSchemes.light.affine.start,
|
||||
[affineIconGradientStop]: colorSchemes.light.affine.stop,
|
||||
},
|
||||
position: 'absolute',
|
||||
zIndex: 0,
|
||||
});
|
||||
globalStyle(`[data-theme='dark'] .${bg} > svg.affine-svg`, {
|
||||
vars: {
|
||||
[affineIconGradientStart]: colorSchemes.dark.affine.start,
|
||||
[affineIconGradientStop]: colorSchemes.dark.affine.stop,
|
||||
},
|
||||
});
|
||||
globalStyle(` .${bg} > svg.icons-svg`, {
|
||||
color: colorSchemes.light.icon,
|
||||
position: 'absolute',
|
||||
zIndex: 2,
|
||||
});
|
||||
globalStyle(`[data-theme='dark'] .${bg} > svg.icons-svg`, {
|
||||
color: colorSchemes.dark.icon,
|
||||
});
|
||||
|
||||
// --------- style1 ---------
|
||||
globalStyle(`.${card}[data-type="1"] .${bg} > svg.affine-svg`, {
|
||||
right: -150,
|
||||
top: -100,
|
||||
});
|
||||
|
||||
globalStyle(`.${card}[data-type="1"] .${bg} > svg.icons-svg`, {
|
||||
right: -20,
|
||||
top: 130,
|
||||
opacity: 0.5,
|
||||
});
|
||||
|
||||
// --------- style2 ---------
|
||||
globalStyle(`.${card}[data-type="2"] .${bg} > svg.affine-svg`, {
|
||||
position: 'absolute',
|
||||
right: -140,
|
||||
bottom: -130,
|
||||
transform: 'scale(0.58)',
|
||||
});
|
||||
|
||||
globalStyle(`.${card}[data-type="2"] .${bg} > svg.icons-svg`, {
|
||||
position: 'absolute',
|
||||
right: 148,
|
||||
bottom: 16,
|
||||
opacity: 0.5,
|
||||
});
|
||||
|
||||
globalStyle(`.${card}[data-type="2"] .${bg} > svg.icons-svg .star`, {
|
||||
display: 'none',
|
||||
});
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { bgAFFiNERaw, bgIconsRaw } from './assets';
|
||||
import { bg, card, content } from './believer-card.css';
|
||||
|
||||
export const BelieverCard = ({
|
||||
children,
|
||||
type,
|
||||
className,
|
||||
...attrs
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
type: 1 | 2;
|
||||
}) => {
|
||||
return (
|
||||
<div className={clsx(card, className)} data-type={type} {...attrs}>
|
||||
<div
|
||||
className={bg}
|
||||
dangerouslySetInnerHTML={{ __html: `${bgAFFiNERaw}${bgIconsRaw}` }}
|
||||
/>
|
||||
<div className={content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const benefits = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
});
|
||||
export const li = style({
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'start',
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
fontWeight: 400,
|
||||
});
|
||||
globalStyle(`.${li} svg`, {
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: cssVar('brandColor'),
|
||||
});
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { AfFiNeIcon, DoneIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { benefits, li } from './benefits.css';
|
||||
|
||||
export const BelieverBenefits = ({
|
||||
className,
|
||||
...attrs
|
||||
}: HTMLAttributes<HTMLUListElement>) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<ul className={clsx(benefits, className)} {...attrs}>
|
||||
<li className={li}>
|
||||
<AfFiNeIcon />
|
||||
<span>{t['com.affine.payment.lifetime.benefit-1']()}</span>
|
||||
</li>
|
||||
|
||||
<li className={li}>
|
||||
<DoneIcon />
|
||||
<span>{t['com.affine.payment.lifetime.benefit-2']()}</span>
|
||||
</li>
|
||||
|
||||
<li className={li}>
|
||||
<DoneIcon />
|
||||
<span>
|
||||
{t['com.affine.payment.lifetime.benefit-3']({
|
||||
capacity: '1T',
|
||||
})}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { SubscriptionRecurring } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import { Upgrade } from '../plan-card';
|
||||
import { BelieverCard } from './believer-card';
|
||||
import { BelieverBenefits } from './benefits';
|
||||
import * as styles from './style.css';
|
||||
|
||||
export const LifetimePlan = () => {
|
||||
const t = useI18n();
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
|
||||
const readableLifetimePrice = useLiveData(
|
||||
subscriptionService.prices.readableLifetimePrice$
|
||||
);
|
||||
const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$);
|
||||
|
||||
if (!readableLifetimePrice) return null;
|
||||
|
||||
return (
|
||||
<BelieverCard type={1}>
|
||||
<div className={styles.caption1}>
|
||||
{t['com.affine.payment.lifetime.caption-1']()}
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
{t['com.affine.payment.lifetime.title']()}
|
||||
</div>
|
||||
|
||||
<div className={styles.price}>{readableLifetimePrice}</div>
|
||||
|
||||
{isBeliever ? (
|
||||
<Button className={styles.purchase} size="default" disabled>
|
||||
{t['com.affine.payment.lifetime.purchased']()}
|
||||
</Button>
|
||||
) : (
|
||||
<Upgrade
|
||||
className={styles.purchase}
|
||||
recurring={SubscriptionRecurring.Lifetime}
|
||||
>
|
||||
{t['com.affine.payment.lifetime.purchase']()}
|
||||
</Upgrade>
|
||||
)}
|
||||
|
||||
<div className={styles.caption2}>
|
||||
<Trans
|
||||
i18nKey="com.affine.payment.lifetime.caption-2"
|
||||
components={{
|
||||
a: <a className={styles.userPolicyLink} href="#" />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BelieverBenefits style={{ padding: '8px 6px' }} />
|
||||
</BelieverCard>
|
||||
);
|
||||
};
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const caption1 = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
marginBottom: 8,
|
||||
});
|
||||
export const title = style({
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
lineHeight: '26px',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
marginBottom: 4,
|
||||
});
|
||||
export const price = style({
|
||||
fontSize: 30,
|
||||
fontWeight: 700,
|
||||
lineHeight: 'normal',
|
||||
color: cssVar('brandColor'),
|
||||
marginBottom: 24,
|
||||
});
|
||||
export const purchase = style({
|
||||
width: 'auto',
|
||||
height: 36,
|
||||
marginBottom: 8,
|
||||
padding: '8px 18px',
|
||||
});
|
||||
export const caption2 = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
fontWeight: 400,
|
||||
marginBottom: 16,
|
||||
maxWidth: 324,
|
||||
});
|
||||
export const userPolicyLink = style({
|
||||
// link not ready
|
||||
// color: cssVar('textPrimaryColor'),
|
||||
// textDecoration: 'underline',
|
||||
color: 'inherit',
|
||||
cursor: 'text',
|
||||
});
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||
import { ConfirmModal, Modal } from '@affine/component/ui/modal';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { DialogTrigger } from '@radix-ui/react-dialog';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export const ConfirmLoadingModal = ({
|
||||
type,
|
||||
loading,
|
||||
open,
|
||||
content,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
...props
|
||||
}: {
|
||||
type: 'resume' | 'change';
|
||||
loading?: boolean;
|
||||
content?: ReactNode;
|
||||
} & ConfirmModalProps) => {
|
||||
const t = useI18n();
|
||||
const confirmed = useRef(false);
|
||||
|
||||
const title = t[`com.affine.payment.modal.${type}.title`]();
|
||||
const confirmText = t[`com.affine.payment.modal.${type}.confirm`]();
|
||||
const cancelText = t[`com.affine.payment.modal.${type}.cancel`]();
|
||||
const contentText =
|
||||
type !== 'change' ? t[`com.affine.payment.modal.${type}.content`]() : '';
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && open && confirmed.current) {
|
||||
onOpenChange?.(false);
|
||||
confirmed.current = false;
|
||||
}
|
||||
}, [loading, open, onOpenChange]);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
title={title}
|
||||
cancelText={cancelText}
|
||||
confirmText={confirmText}
|
||||
confirmButtonOptions={{
|
||||
variant: 'primary',
|
||||
loading,
|
||||
}}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onConfirm={() => {
|
||||
confirmed.current = true;
|
||||
onConfirm?.()?.catch(console.error);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{content ?? contentText}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Downgrade modal, confirm & cancel button are reversed
|
||||
* @param param0
|
||||
*/
|
||||
export const DowngradeModal = ({
|
||||
open,
|
||||
loading,
|
||||
onOpenChange,
|
||||
onCancel,
|
||||
}: {
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
onCancel?: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const canceled = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && open && canceled.current) {
|
||||
onOpenChange?.(false);
|
||||
canceled.current = false;
|
||||
}
|
||||
}, [loading, open, onOpenChange]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t['com.affine.payment.modal.downgrade.title']()}
|
||||
open={open}
|
||||
contentOptions={{}}
|
||||
width={480}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<div className={styles.downgradeContentWrapper}>
|
||||
<p className={styles.downgradeContent}>
|
||||
{t['com.affine.payment.modal.downgrade.content']()}
|
||||
</p>
|
||||
<p className={styles.downgradeCaption}>
|
||||
{t['com.affine.payment.modal.downgrade.caption']()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer className={styles.downgradeFooter}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
canceled.current = true;
|
||||
onCancel?.();
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
{t['com.affine.payment.modal.downgrade.cancel']()}
|
||||
</Button>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
disabled={loading}
|
||||
onClick={() => onOpenChange?.(false)}
|
||||
variant="primary"
|
||||
>
|
||||
{t['com.affine.payment.modal.downgrade.confirm']()}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
-456
@@ -1,456 +0,0 @@
|
||||
import { Button, type ButtonProps } from '@affine/component/ui/button';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type CreateCheckoutSessionInput,
|
||||
SubscriptionRecurring,
|
||||
} from '@affine/graphql';
|
||||
import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionStatus,
|
||||
SubscriptionVariant,
|
||||
} from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { DoneIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms/index';
|
||||
import { CancelAction, ResumeAction } from './actions';
|
||||
import { CheckoutSlot } from './checkout-slot';
|
||||
import type { DynamicPrice, FixedPrice } from './cloud-plans';
|
||||
import { ConfirmLoadingModal } from './modals';
|
||||
import * as styles from './style.css';
|
||||
|
||||
interface PlanCardProps {
|
||||
detail: FixedPrice | DynamicPrice;
|
||||
recurring: SubscriptionRecurring;
|
||||
}
|
||||
|
||||
export const PlanCard = (props: PlanCardProps) => {
|
||||
const { detail, recurring } = props;
|
||||
const loggedIn =
|
||||
useLiveData(useService(AuthService).session.status$) === 'authenticated';
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const proSubscription = useLiveData(subscriptionService.subscription.pro$);
|
||||
const currentPlan = proSubscription?.plan ?? SubscriptionPlan.Free;
|
||||
|
||||
const isCurrent =
|
||||
loggedIn &&
|
||||
detail.plan === currentPlan &&
|
||||
recurring === proSubscription?.recurring;
|
||||
const isPro = detail.plan === SubscriptionPlan.Pro;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-current={isCurrent}
|
||||
key={detail.plan}
|
||||
className={isPro ? styles.proPlanCard : styles.planCard}
|
||||
>
|
||||
<div className={styles.planCardBorderMock} />
|
||||
<div className={styles.planTitle}>
|
||||
<div style={{ paddingBottom: 12 }}>
|
||||
<section className={styles.planTitleName}>{detail.name}</section>
|
||||
<section className={styles.planTitleDescription}>
|
||||
{detail.description}
|
||||
</section>
|
||||
<section className={styles.planTitleTitle}>
|
||||
{detail.titleRenderer(recurring, detail as any)}
|
||||
</section>
|
||||
</div>
|
||||
<ActionButton {...props} />
|
||||
</div>
|
||||
<div className={styles.planBenefits}>
|
||||
{Object.entries(detail.benefits).map(([groupName, benefitList]) => {
|
||||
return (
|
||||
<ul className={styles.planBenefitGroup} key={groupName}>
|
||||
<section className={styles.planBenefitGroupTitle}>
|
||||
{groupName}:
|
||||
</section>
|
||||
{benefitList.map(({ icon, title }, index) => {
|
||||
return (
|
||||
<li className={styles.planBenefit} key={index}>
|
||||
<div className={styles.planBenefitIcon}>
|
||||
{icon ?? <DoneIcon />}
|
||||
</div>
|
||||
<div className={styles.planBenefitText}>{title}</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionButton = ({ detail, recurring }: PlanCardProps) => {
|
||||
const t = useI18n();
|
||||
const loggedIn =
|
||||
useLiveData(useService(AuthService).session.status$) === 'authenticated';
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$);
|
||||
const primarySubscription = useLiveData(
|
||||
subscriptionService.subscription.pro$
|
||||
);
|
||||
const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free;
|
||||
const currentRecurring = primarySubscription?.recurring;
|
||||
const isOnetime = useLiveData(subscriptionService.subscription.isOnetimePro$);
|
||||
const isFree = detail.plan === SubscriptionPlan.Free;
|
||||
|
||||
// branches:
|
||||
// if contact => 'Contact Sales'
|
||||
// if not signed in:
|
||||
// if free => 'Sign up free'
|
||||
// else => 'Buy Pro'
|
||||
// else
|
||||
// if isBeliever => 'Included in Lifetime'
|
||||
// if onetime
|
||||
// if free => 'Included in Pro'
|
||||
// else => 'Redeem Code'
|
||||
// if isCurrent
|
||||
// if canceled => 'Resume'
|
||||
// else => 'Current Plan'
|
||||
// if free => 'Downgrade'
|
||||
// if currentRecurring !== recurring => 'Change to {recurring} Billing'
|
||||
// else => 'Upgrade'
|
||||
|
||||
// contact
|
||||
if (detail.type === 'dynamic') {
|
||||
return <BookDemo plan={detail.plan} />;
|
||||
}
|
||||
|
||||
// not signed in
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<SignUpAction>
|
||||
{detail.plan === SubscriptionPlan.Free
|
||||
? t['com.affine.payment.sign-up-free']()
|
||||
: t['com.affine.payment.buy-pro']()}
|
||||
</SignUpAction>
|
||||
);
|
||||
}
|
||||
|
||||
// lifetime
|
||||
if (isBeliever) {
|
||||
return (
|
||||
<Button className={styles.planAction} disabled>
|
||||
{t['com.affine.payment.cloud.lifetime.included']()}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// onetime
|
||||
if (isOnetime) {
|
||||
return isFree ? (
|
||||
<Button className={styles.planAction} disabled>
|
||||
{t['com.affine.payment.cloud.onetime.included']()}
|
||||
</Button>
|
||||
) : (
|
||||
<RedeemCode recurring={recurring} />
|
||||
);
|
||||
}
|
||||
|
||||
const isCanceled = !!primarySubscription?.canceledAt;
|
||||
const isCurrent =
|
||||
detail.plan === currentPlan &&
|
||||
(isFree
|
||||
? true
|
||||
: currentRecurring === recurring &&
|
||||
primarySubscription?.status === SubscriptionStatus.Active);
|
||||
|
||||
// is current
|
||||
if (isCurrent) {
|
||||
return isCanceled ? <ResumeButton /> : <CurrentPlan />;
|
||||
}
|
||||
|
||||
if (isFree) {
|
||||
return <Downgrade disabled={isCanceled} />;
|
||||
}
|
||||
|
||||
return currentPlan === detail.plan ? (
|
||||
<ChangeRecurring
|
||||
from={currentRecurring as SubscriptionRecurring}
|
||||
to={recurring as SubscriptionRecurring}
|
||||
due={primarySubscription?.nextBillAt || ''}
|
||||
disabled={isCanceled}
|
||||
/>
|
||||
) : (
|
||||
<Upgrade recurring={recurring as SubscriptionRecurring} />
|
||||
);
|
||||
};
|
||||
|
||||
const CurrentPlan = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<Button className={styles.planAction}>
|
||||
{t['com.affine.payment.current-plan']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const Downgrade = ({ disabled }: { disabled?: boolean }) => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const tooltipContent = disabled
|
||||
? t['com.affine.payment.downgraded-tooltip']()
|
||||
: null;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CancelAction open={open} onOpenChange={setOpen}>
|
||||
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
|
||||
<div className={styles.planAction}>
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
variant="primary"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t['com.affine.payment.downgrade']()}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</CancelAction>
|
||||
);
|
||||
};
|
||||
|
||||
const BookDemo = ({ plan }: { plan: SubscriptionPlan }) => {
|
||||
const t = useI18n();
|
||||
const url = useMemo(() => {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.Team:
|
||||
return 'https://6dxre9ihosp.typeform.com/to/niBcdkvs';
|
||||
case SubscriptionPlan.Enterprise:
|
||||
return 'https://6dxre9ihosp.typeform.com/to/rFfobTjf';
|
||||
default:
|
||||
return 'https://affine.pro/pricing';
|
||||
}
|
||||
}, [plan]);
|
||||
|
||||
return (
|
||||
<a
|
||||
className={styles.planAction}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
variant="primary"
|
||||
data-event-props="$.settingsPanel.billing.bookDemo"
|
||||
data-event-args-url={url}
|
||||
>
|
||||
{t['com.affine.payment.tell-us-use-case']()}
|
||||
</Button>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const Upgrade = ({
|
||||
className,
|
||||
recurring,
|
||||
children,
|
||||
checkoutInput,
|
||||
...btnProps
|
||||
}: ButtonProps & {
|
||||
recurring: SubscriptionRecurring;
|
||||
checkoutInput?: Partial<CreateCheckoutSessionInput>;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const onBeforeCheckout = useCallback(() => {
|
||||
track.$.settingsPanel.plans.checkout({
|
||||
plan: SubscriptionPlan.Pro,
|
||||
recurring: recurring,
|
||||
});
|
||||
}, [recurring]);
|
||||
|
||||
const checkoutOptions = useMemo(
|
||||
() => ({
|
||||
recurring,
|
||||
plan: SubscriptionPlan.Pro,
|
||||
variant: null,
|
||||
coupon: null,
|
||||
successCallbackLink: generateSubscriptionCallbackLink(
|
||||
authService.session.account$.value,
|
||||
SubscriptionPlan.Pro,
|
||||
recurring
|
||||
),
|
||||
...checkoutInput,
|
||||
}),
|
||||
[authService.session.account$.value, checkoutInput, recurring]
|
||||
);
|
||||
|
||||
return (
|
||||
<CheckoutSlot
|
||||
onBeforeCheckout={onBeforeCheckout}
|
||||
checkoutOptions={checkoutOptions}
|
||||
renderer={props => (
|
||||
<Button
|
||||
className={clsx(styles.planAction, className)}
|
||||
variant="primary"
|
||||
{...props}
|
||||
{...btnProps}
|
||||
>
|
||||
{children ?? t['com.affine.payment.upgrade']()}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangeRecurring = ({
|
||||
from,
|
||||
to,
|
||||
disabled,
|
||||
due,
|
||||
}: {
|
||||
from: SubscriptionRecurring;
|
||||
to: SubscriptionRecurring;
|
||||
disabled?: boolean;
|
||||
due: string;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
// allow replay request on network error until component unmount or success
|
||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const onStartChange = useCallback(() => {
|
||||
track.$.settingsPanel.plans.changeSubscriptionRecurring({
|
||||
plan: SubscriptionPlan.Pro,
|
||||
recurring: to,
|
||||
});
|
||||
setOpen(true);
|
||||
}, [to]);
|
||||
|
||||
const change = useAsyncCallback(async () => {
|
||||
setIsMutating(true);
|
||||
await subscription.setSubscriptionRecurring(idempotencyKey, to);
|
||||
setIdempotencyKey(nanoid());
|
||||
setIsMutating(false);
|
||||
}, [subscription, to, idempotencyKey]);
|
||||
|
||||
const changeCurringContent = (
|
||||
<Trans values={{ from, to, due }} className={styles.downgradeContent}>
|
||||
You are changing your <span className={styles.textEmphasis}>{from}</span>{' '}
|
||||
subscription to <span className={styles.textEmphasis}>{to}</span>{' '}
|
||||
subscription. This change will take effect in the next billing cycle, with
|
||||
an effective date of{' '}
|
||||
<span className={styles.textEmphasis}>
|
||||
{new Date(due).toLocaleDateString()}
|
||||
</span>
|
||||
.
|
||||
</Trans>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className={styles.planAction}
|
||||
variant="primary"
|
||||
onClick={onStartChange}
|
||||
disabled={disabled || isMutating}
|
||||
loading={isMutating}
|
||||
>
|
||||
{t['com.affine.payment.change-to']({ to })}
|
||||
</Button>
|
||||
|
||||
<ConfirmLoadingModal
|
||||
type={'change'}
|
||||
loading={isMutating}
|
||||
open={open}
|
||||
onConfirm={change}
|
||||
onOpenChange={setOpen}
|
||||
content={changeCurringContent}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SignUpAction = ({ children }: PropsWithChildren) => {
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
|
||||
const onClickSignIn = useCallback(() => {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}, [setOpen]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onClickSignIn}
|
||||
className={styles.planAction}
|
||||
variant="primary"
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const ResumeButton = () => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
const subscription = useService(SubscriptionService).subscription;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
const pro = subscription.pro$.value;
|
||||
if (pro) {
|
||||
track.$.settingsPanel.plans.resumeSubscription({
|
||||
plan: SubscriptionPlan.Pro,
|
||||
recurring: pro.recurring,
|
||||
});
|
||||
}
|
||||
}, [subscription.pro$.value]);
|
||||
|
||||
return (
|
||||
<ResumeAction open={open} onOpenChange={setOpen}>
|
||||
<Button className={styles.resumeAction} onClick={handleClick}>
|
||||
<span data-show-hover="true" className={clsx(styles.resumeContent)}>
|
||||
{t['com.affine.payment.resume-renewal']()}
|
||||
</span>
|
||||
<span data-show-hover="false" className={clsx(styles.resumeContent)}>
|
||||
{t['com.affine.payment.current-plan']()}
|
||||
</span>
|
||||
</Button>
|
||||
</ResumeAction>
|
||||
);
|
||||
};
|
||||
|
||||
const redeemCodeCheckoutInput = { variant: SubscriptionVariant.Onetime };
|
||||
export const RedeemCode = ({
|
||||
className,
|
||||
recurring = SubscriptionRecurring.Yearly,
|
||||
children,
|
||||
...btnProps
|
||||
}: ButtonProps & { recurring?: SubscriptionRecurring }) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Upgrade
|
||||
recurring={recurring}
|
||||
className={className}
|
||||
checkoutInput={redeemCodeCheckoutInput}
|
||||
{...btnProps}
|
||||
>
|
||||
{children ?? t['com.affine.payment.redeem-code']()}
|
||||
</Upgrade>
|
||||
);
|
||||
};
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const plansWrapper = style({
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
});
|
||||
export const planItemCard = style({
|
||||
width: '258px',
|
||||
height: '426px',
|
||||
flexShrink: '0',
|
||||
borderRadius: '16px',
|
||||
backgroundColor: cssVar('backgroundPrimaryColor'),
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
});
|
||||
export const planItemHeader = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
});
|
||||
export const planItemContent = style({
|
||||
flexGrow: '1',
|
||||
height: 0,
|
||||
});
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
import { Skeleton } from '@affine/component';
|
||||
|
||||
import { AIPlanLayout } from './ai/layout';
|
||||
import { CloudPlanLayout, PlanLayout } from './layout';
|
||||
import * as styles from './skeleton.css';
|
||||
|
||||
/**
|
||||
* Customize Skeleton component with rounded border radius
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
const RoundedSkeleton = ({
|
||||
radius = 8,
|
||||
...props
|
||||
}: {
|
||||
radius?: number;
|
||||
} & React.ComponentProps<typeof Skeleton>) => (
|
||||
<Skeleton {...props} style={{ borderRadius: `${radius}px` }} />
|
||||
);
|
||||
|
||||
const TabsSkeleton = () => (
|
||||
// TODO(@catsjuice): height should be `32px` by design
|
||||
// but the RadioGroup component is not matching with the design currently
|
||||
// set to `24px` for now to avoid blinking
|
||||
<Skeleton variant="rounded" width="256px" height="24px" />
|
||||
);
|
||||
|
||||
const PlanItemSkeleton = () => (
|
||||
<div className={styles.planItemCard}>
|
||||
<header className={styles.planItemHeader}>
|
||||
<RoundedSkeleton variant="rounded" width="100%" height="60px" />
|
||||
<RoundedSkeleton variant="rounded" width="100%" height="28px" />
|
||||
</header>
|
||||
|
||||
<main className={styles.planItemContent}>
|
||||
<RoundedSkeleton variant="rounded" width="100%" height="100%" />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ScrollSkeleton = () => (
|
||||
<div className={styles.plansWrapper}>
|
||||
<PlanItemSkeleton />
|
||||
<PlanItemSkeleton />
|
||||
<PlanItemSkeleton />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PlansSkeleton = () => {
|
||||
return (
|
||||
<PlanLayout
|
||||
ai={
|
||||
<AIPlanLayout
|
||||
caption={
|
||||
<RoundedSkeleton
|
||||
variant="rectangular"
|
||||
radius={2}
|
||||
width="200px"
|
||||
height="20px"
|
||||
/>
|
||||
}
|
||||
actionButtons={
|
||||
<>
|
||||
<RoundedSkeleton
|
||||
variant="rectangular"
|
||||
radius={20}
|
||||
width="206px"
|
||||
height="37px"
|
||||
/>
|
||||
<RoundedSkeleton
|
||||
variant="rectangular"
|
||||
radius={20}
|
||||
width="193px"
|
||||
height="37px"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
cloud={
|
||||
<CloudPlanLayout
|
||||
toggle={
|
||||
<RoundedSkeleton variant="rounded" width="100%" height="32px" />
|
||||
}
|
||||
select={<TabsSkeleton />}
|
||||
scroll={<ScrollSkeleton />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-266
@@ -1,266 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const wrapper = style({
|
||||
width: '100%',
|
||||
});
|
||||
export const recurringToggleWrapper = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
minHeight: 40,
|
||||
});
|
||||
// export const recurringToggleLabel = style({});
|
||||
export const recurringToggleRecurring = style({
|
||||
fontWeight: 400,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
export const recurringToggleDiscount = style({
|
||||
fontWeight: 600,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('brandColor'),
|
||||
});
|
||||
export const radioButtonDiscount = style({
|
||||
marginLeft: '4px',
|
||||
color: cssVar('brandColor'),
|
||||
fontWeight: 400,
|
||||
});
|
||||
export const radioButtonText = style({
|
||||
selectors: {
|
||||
'&:first-letter': {
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const cloudSelect = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
});
|
||||
globalStyle(`.${cloudSelect} > span`, { color: cssVar('textSecondaryColor') });
|
||||
export const planCardsWrapper = style({
|
||||
paddingRight: 'calc(var(--setting-modal-gap-x) + 30px)',
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
width: 'fit-content',
|
||||
});
|
||||
export const planCard = style({
|
||||
backgroundColor: cssVar('backgroundPrimaryColor'),
|
||||
minHeight: '426px',
|
||||
minWidth: '258px',
|
||||
borderRadius: '16px',
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
transition: 'all 0.23s ease',
|
||||
selectors: {
|
||||
'&::before': {
|
||||
content: '',
|
||||
position: 'absolute',
|
||||
right: 'calc(100% + var(--setting-modal-gap-x))',
|
||||
scrollSnapAlign: 'start',
|
||||
},
|
||||
'&[data-current="true"]': {
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const planCardBorderMock = style({
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: 'inherit',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1,
|
||||
|
||||
'::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 'inherit',
|
||||
border: `2px solid transparent`,
|
||||
// TODO(@catsjuice): brandColor with opacity, dark mode compatibility needed
|
||||
background: `linear-gradient(180deg, ${cssVar('brandColor')}, #1E96EB33) border-box`,
|
||||
['WebkitMask']: `linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)`,
|
||||
[`WebkitMaskComposite`]: `destination-out`,
|
||||
maskComposite: `exclude`,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.23s ease',
|
||||
},
|
||||
|
||||
selectors: {
|
||||
[`.${planCard}[data-current="true"] &::after`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const proPlanCard = style([planCard, {}]);
|
||||
export const proPlanTitle = style({
|
||||
backgroundColor: cssVar('brandColor'),
|
||||
color: cssVar('white'),
|
||||
padding: '0px 6px',
|
||||
borderRadius: '4px',
|
||||
height: '24px',
|
||||
display: 'inline-block',
|
||||
});
|
||||
export const discountLabel = style({
|
||||
color: cssVar('textEmphasisColor'),
|
||||
marginLeft: '8px',
|
||||
lineHeight: '20px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 500,
|
||||
borderRadius: '4px',
|
||||
display: 'inline-block',
|
||||
height: '100%',
|
||||
});
|
||||
export const planTitle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
padding: '12px 16px',
|
||||
background: cssVar('backgroundOverlayPanelColor'),
|
||||
borderRadius: 'inherit',
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottom: '1px solid ' + cssVar('borderColor'),
|
||||
fontWeight: 600,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
});
|
||||
export const planTitleSpotlight = style({});
|
||||
globalStyle(`.${planTitle} > :not(.${planTitleSpotlight})`, {
|
||||
position: 'relative',
|
||||
});
|
||||
export const planTitleName = style({
|
||||
fontWeight: 600,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
});
|
||||
export const planTitleDescription = style({
|
||||
fontWeight: 400,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
marginBottom: 8,
|
||||
});
|
||||
export const planTitleTitle = style({
|
||||
fontWeight: 600,
|
||||
fontSize: cssVar('fontBase'),
|
||||
lineHeight: '20px',
|
||||
height: 20,
|
||||
});
|
||||
export const planTitleTitleCaption = style({
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
marginLeft: 4,
|
||||
});
|
||||
export const planPriceWrapper = style({
|
||||
minHeight: '28px',
|
||||
lineHeight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
});
|
||||
export const planPrice = style({
|
||||
fontSize: cssVar('fontH5'),
|
||||
marginRight: '8px',
|
||||
});
|
||||
export const planPriceDesc = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
});
|
||||
export const planAction = style({
|
||||
width: '100%',
|
||||
});
|
||||
export const resumeAction = style([planAction, {}]);
|
||||
export const resumeContent = style({
|
||||
selectors: {
|
||||
[`&[data-show-hover="true"], ${resumeAction}:hover &[data-show-hover="false"]`]:
|
||||
{
|
||||
display: 'none',
|
||||
},
|
||||
[`&[data-show-hover="false"], ${resumeAction}:hover &[data-show-hover="true"]`]:
|
||||
{
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const planBenefits = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
padding: '12px 16px',
|
||||
});
|
||||
export const planBenefitGroup = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
});
|
||||
export const planBenefitGroupTitle = style({
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
export const planBenefit = style({
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
lineHeight: '20px',
|
||||
alignItems: 'normal',
|
||||
});
|
||||
export const planBenefitIcon = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '20px',
|
||||
});
|
||||
globalStyle(`.${planBenefitIcon} > svg`, {
|
||||
color: cssVar('brandColor'),
|
||||
});
|
||||
export const planBenefitText = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
export const downgradeContentWrapper = style({
|
||||
padding: '12px 0 20px 0px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '12px',
|
||||
});
|
||||
export const downgradeContent = style({
|
||||
fontSize: '15px',
|
||||
lineHeight: '24px',
|
||||
fontWeight: 400,
|
||||
color: cssVar('textPrimaryColor'),
|
||||
});
|
||||
export const downgradeCaption = style({
|
||||
fontSize: '14px',
|
||||
lineHeight: '22px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
export const downgradeFooter = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '20px',
|
||||
paddingTop: '20px',
|
||||
});
|
||||
export const textEmphasis = style({
|
||||
color: cssVar('textEmphasisColor'),
|
||||
});
|
||||
export const errorTip = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: '12px',
|
||||
lineHeight: '20px',
|
||||
});
|
||||
export const errorTipRetry = style({
|
||||
textDecoration: 'underline',
|
||||
});
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
|
||||
import type { ShortcutsInfo } from '../../../../../components/hooks/affine/use-shortcuts';
|
||||
import {
|
||||
useEdgelessShortcuts,
|
||||
useGeneralShortcuts,
|
||||
useMarkdownShortcuts,
|
||||
usePageShortcuts,
|
||||
} from '../../../../../components/hooks/affine/use-shortcuts';
|
||||
import { shortcutKey, shortcutKeyContainer, shortcutRow } from './style.css';
|
||||
|
||||
const ShortcutsPanel = ({
|
||||
shortcutsInfo,
|
||||
}: {
|
||||
shortcutsInfo: ShortcutsInfo;
|
||||
}) => {
|
||||
return (
|
||||
<SettingWrapper title={shortcutsInfo.title}>
|
||||
{Object.entries(shortcutsInfo.shortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<div key={title} className={shortcutRow}>
|
||||
<span>{title}</span>
|
||||
<div className={shortcutKeyContainer}>
|
||||
{shortcuts.map(key => {
|
||||
return (
|
||||
<span className={shortcutKey} key={key}>
|
||||
{key}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</SettingWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const Shortcuts = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const markdownShortcutsInfo = useMarkdownShortcuts();
|
||||
const pageShortcutsInfo = usePageShortcuts();
|
||||
const edgelessShortcutsInfo = useEdgelessShortcuts();
|
||||
const generalShortcutsInfo = useGeneralShortcuts();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['com.affine.keyboardShortcuts.title']()}
|
||||
subtitle={t['com.affine.keyboardShortcuts.subtitle']()}
|
||||
data-testid="keyboard-shortcuts-title"
|
||||
/>
|
||||
<ShortcutsPanel shortcutsInfo={generalShortcutsInfo} />
|
||||
<ShortcutsPanel shortcutsInfo={pageShortcutsInfo} />
|
||||
<ShortcutsPanel shortcutsInfo={edgelessShortcutsInfo} />
|
||||
<ShortcutsPanel shortcutsInfo={markdownShortcutsInfo} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const shortcutRow = style({
|
||||
height: '32px',
|
||||
marginBottom: '12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: cssVar('fontBase'),
|
||||
selectors: {
|
||||
'&:last-of-type': {
|
||||
marginBottom: '0',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const shortcutKeyContainer = style({
|
||||
display: 'flex',
|
||||
});
|
||||
export const shortcutKey = style({
|
||||
minWidth: '24px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0 6px',
|
||||
borderRadius: '4px',
|
||||
background: cssVar('backgroundTertiaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
selectors: {
|
||||
'&:not(:last-of-type)': {
|
||||
marginRight: '2px',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,226 +0,0 @@
|
||||
import { Loading, Scrollable } from '@affine/component';
|
||||
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
||||
import type { ModalProps } from '@affine/component/ui/modal';
|
||||
import { Modal } from '@affine/component/ui/modal';
|
||||
import {
|
||||
openIssueFeedbackModalAtom,
|
||||
openStarAFFiNEModalAtom,
|
||||
} from '@affine/core/components/atoms';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
type WorkspaceMetadata,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { debounce } from 'lodash-es';
|
||||
import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { AccountSetting } from './account-setting';
|
||||
import { settingModalScrollContainerAtom } from './atoms';
|
||||
import { GeneralSetting } from './general-setting';
|
||||
import { SettingSidebar } from './setting-sidebar';
|
||||
import * as style from './style.css';
|
||||
import type { ActiveTab, GeneralSettingKey, WorkspaceSubTab } from './types';
|
||||
import { GeneralSettingKeys } from './types';
|
||||
import { WorkspaceSetting } from './workspace-setting';
|
||||
|
||||
export interface SettingProps extends ModalProps {
|
||||
activeTab: ActiveTab;
|
||||
workspaceMetadata?: WorkspaceMetadata | null;
|
||||
onSettingClick: (params: {
|
||||
activeTab: ActiveTab;
|
||||
workspaceMetadata: WorkspaceMetadata | null;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const isGeneralSetting = (key: string): key is GeneralSettingKey =>
|
||||
GeneralSettingKeys.includes(key as GeneralSettingKey);
|
||||
|
||||
const CenteredLoading = () => {
|
||||
return (
|
||||
<div className={style.centeredLoading}>
|
||||
<Loading size={24} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingModalInner = ({
|
||||
activeTab = 'appearance',
|
||||
workspaceMetadata = null,
|
||||
onSettingClick,
|
||||
...modalProps
|
||||
}: SettingProps) => {
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
|
||||
const modalContentRef = useRef<HTMLDivElement>(null);
|
||||
const modalContentWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const setSettingModalScrollContainer = useSetAtom(
|
||||
settingModalScrollContainerAtom
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!modalProps.open) return;
|
||||
let animationFrameId: number;
|
||||
const onResize = debounce(() => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = requestAnimationFrame(() => {
|
||||
if (!modalContentRef.current || !modalContentWrapperRef.current) return;
|
||||
|
||||
const wrapperWidth = modalContentWrapperRef.current.offsetWidth;
|
||||
const wrapperHeight = modalContentWrapperRef.current.offsetHeight;
|
||||
const contentWidth = modalContentRef.current.offsetWidth;
|
||||
|
||||
const wrapper = modalContentWrapperRef.current;
|
||||
|
||||
wrapper?.style.setProperty(
|
||||
'--setting-modal-width',
|
||||
`${wrapperWidth}px`
|
||||
);
|
||||
wrapper?.style.setProperty(
|
||||
'--setting-modal-height',
|
||||
`${wrapperHeight}px`
|
||||
);
|
||||
wrapper?.style.setProperty(
|
||||
'--setting-modal-content-width',
|
||||
`${contentWidth}px`
|
||||
);
|
||||
wrapper?.style.setProperty(
|
||||
'--setting-modal-gap-x',
|
||||
`${(wrapperWidth - contentWidth) / 2}px`
|
||||
);
|
||||
});
|
||||
}, 200);
|
||||
window.addEventListener('resize', onResize);
|
||||
onResize();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}, [modalProps.open]);
|
||||
|
||||
useEffect(() => {
|
||||
setSettingModalScrollContainer(modalContentWrapperRef.current);
|
||||
return () => {
|
||||
setSettingModalScrollContainer(null);
|
||||
};
|
||||
}, [setSettingModalScrollContainer]);
|
||||
|
||||
const onTabChange = useCallback(
|
||||
(key: ActiveTab, meta: WorkspaceMetadata | null) => {
|
||||
onSettingClick({ activeTab: key, workspaceMetadata: meta });
|
||||
},
|
||||
[onSettingClick]
|
||||
);
|
||||
const setOpenIssueFeedbackModal = useSetAtom(openIssueFeedbackModalAtom);
|
||||
const setOpenStarAFFiNEModal = useSetAtom(openStarAFFiNEModalAtom);
|
||||
|
||||
const handleOpenIssueFeedbackModal = useCallback(() => {
|
||||
setOpenIssueFeedbackModal(true);
|
||||
}, [setOpenIssueFeedbackModal]);
|
||||
|
||||
const handleOpenStarAFFiNEModal = useCallback(() => {
|
||||
setOpenStarAFFiNEModal(true);
|
||||
}, [setOpenStarAFFiNEModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingSidebar
|
||||
activeTab={activeTab}
|
||||
onTabChange={onTabChange}
|
||||
selectedWorkspaceId={workspaceMetadata?.id ?? null}
|
||||
/>
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport
|
||||
data-testid="setting-modal-content"
|
||||
className={style.wrapper}
|
||||
ref={modalContentWrapperRef}
|
||||
>
|
||||
<div ref={modalContentRef} className={style.centerContainer}>
|
||||
<div className={style.content}>
|
||||
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
||||
{activeTab.startsWith('workspace:') && workspaceMetadata ? (
|
||||
<WorkspaceSetting
|
||||
subTab={activeTab.split(':')[1] as WorkspaceSubTab}
|
||||
key={workspaceMetadata.id}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
/>
|
||||
) : null}
|
||||
{isGeneralSetting(activeTab) ? (
|
||||
<GeneralSetting generalKey={activeTab} />
|
||||
) : null}
|
||||
{activeTab === 'account' && loginStatus === 'authenticated' ? (
|
||||
<AccountSetting />
|
||||
) : null}
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className={style.footer}>
|
||||
<ContactWithUsIcon fontSize={16} />
|
||||
<Trans
|
||||
i18nKey={'com.affine.settings.suggestion-2'}
|
||||
components={{
|
||||
1: (
|
||||
<span
|
||||
className={style.link}
|
||||
onClick={handleOpenStarAFFiNEModal}
|
||||
/>
|
||||
),
|
||||
2: (
|
||||
<span
|
||||
className={style.link}
|
||||
onClick={handleOpenIssueFeedbackModal}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Viewport>
|
||||
</Scrollable.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingModal = ({
|
||||
activeTab = 'appearance',
|
||||
workspaceMetadata = null,
|
||||
onSettingClick,
|
||||
...modalProps
|
||||
}: SettingProps) => {
|
||||
return (
|
||||
<Modal
|
||||
width={1280}
|
||||
height={920}
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'setting-modal',
|
||||
style: {
|
||||
maxHeight: '85vh',
|
||||
maxWidth: '70vw',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
{...modalProps}
|
||||
>
|
||||
<Suspense fallback={<CenteredLoading />}>
|
||||
<SettingModalInner
|
||||
activeTab={activeTab}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
onSettingClick={onSettingClick}
|
||||
{...modalProps}
|
||||
/>
|
||||
</Suspense>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,332 +0,0 @@
|
||||
import {
|
||||
WorkspaceListItemSkeleton,
|
||||
WorkspaceListSkeleton,
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { Logo1Icon } from '@blocksuite/icons/rc';
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import {
|
||||
type MouseEvent,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { authAtom } from '../../../atoms';
|
||||
import { UserPlanButton } from '../../auth/user-plan-button';
|
||||
import { useGeneralSettingList } from '../general-setting';
|
||||
import type { ActiveTab, WorkspaceSubTab } from '../types';
|
||||
import * as style from './style.css';
|
||||
|
||||
export type UserInfoProps = {
|
||||
onAccountSettingClick: () => void;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export const UserInfo = ({ onAccountSettingClick, active }: UserInfoProps) => {
|
||||
const account = useLiveData(useService(AuthService).session.account$);
|
||||
if (!account) {
|
||||
// TODO(@eyhn): loading ui
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-testid="user-info-card"
|
||||
className={clsx(style.accountButton, {
|
||||
active: active,
|
||||
})}
|
||||
onClick={onAccountSettingClick}
|
||||
>
|
||||
<Avatar
|
||||
size={28}
|
||||
rounded={2}
|
||||
name={account.label}
|
||||
url={account.avatar}
|
||||
className="avatar"
|
||||
/>
|
||||
|
||||
<div className="content">
|
||||
<div className="name-container">
|
||||
<div className="name" title={account.label}>
|
||||
{account.label}
|
||||
</div>
|
||||
<UserPlanButton />
|
||||
</div>
|
||||
|
||||
<div className="email" title={account.email}>
|
||||
{account.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SignInButton = () => {
|
||||
const t = useI18n();
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={style.accountButton}
|
||||
onClick={useCallback(() => {
|
||||
setAuthModal({ openModal: true, state: 'signIn' });
|
||||
}, [setAuthModal])}
|
||||
>
|
||||
<div className="avatar not-sign">
|
||||
<Logo1Icon />
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<div className="name" title={t['com.affine.settings.sign']()}>
|
||||
{t['com.affine.settings.sign']()}
|
||||
</div>
|
||||
<div className="email" title={t['com.affine.setting.sign.message']()}>
|
||||
{t['com.affine.setting.sign.message']()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingSidebar = ({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
selectedWorkspaceId,
|
||||
}: {
|
||||
activeTab: ActiveTab;
|
||||
onTabChange: (
|
||||
key: ActiveTab,
|
||||
workspaceMetadata: WorkspaceMetadata | null
|
||||
) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
const generalList = useGeneralSettingList();
|
||||
const gotoTab = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
const tab = e.currentTarget.dataset.eventArg;
|
||||
if (!tab) return;
|
||||
track.$.settingsPanel.menu.openSettings({ to: tab });
|
||||
onTabChange(tab as ActiveTab, null);
|
||||
},
|
||||
[onTabChange]
|
||||
);
|
||||
const onAccountSettingClick = useCallback(() => {
|
||||
track.$.settingsPanel.menu.openSettings({ to: 'account' });
|
||||
onTabChange('account', null);
|
||||
}, [onTabChange]);
|
||||
const onWorkspaceSettingClick = useCallback(
|
||||
(subTab: WorkspaceSubTab, workspaceMetadata: WorkspaceMetadata) => {
|
||||
track.$.settingsPanel.menu.openSettings({
|
||||
to: 'workspace',
|
||||
control: subTab,
|
||||
});
|
||||
onTabChange(`workspace:${subTab}`, workspaceMetadata);
|
||||
},
|
||||
[onTabChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={style.settingSlideBar} data-testid="settings-sidebar">
|
||||
<div className={style.sidebarTitle}>
|
||||
{t['com.affine.settingSidebar.title']()}
|
||||
</div>
|
||||
<div className={style.sidebarSubtitle}>
|
||||
{t['com.affine.settingSidebar.settings.general']()}
|
||||
</div>
|
||||
<div className={style.sidebarItemsWrapper}>
|
||||
{generalList.map(({ title, icon, key, testId }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(style.sidebarSelectItem, {
|
||||
active: key === activeTab,
|
||||
})}
|
||||
key={key}
|
||||
title={title}
|
||||
data-event-arg={key}
|
||||
onClick={gotoTab}
|
||||
data-testid={testId}
|
||||
>
|
||||
{icon({ className: 'icon' })}
|
||||
<span className="setting-name">{title}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={style.sidebarSubtitle}>
|
||||
{t['com.affine.settingSidebar.settings.workspace']()}
|
||||
</div>
|
||||
<div className={clsx(style.sidebarItemsWrapper, 'scroll')}>
|
||||
<Suspense fallback={<WorkspaceListSkeleton />}>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
activeSubTab={activeTab.split(':')[1] as WorkspaceSubTab}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className={style.sidebarFooter}>
|
||||
{loginStatus === 'unauthenticated' ? <SignInButton /> : null}
|
||||
{loginStatus === 'authenticated' ? (
|
||||
<Suspense>
|
||||
<UserInfo
|
||||
onAccountSettingClick={onAccountSettingClick}
|
||||
active={activeTab === 'account'}
|
||||
/>
|
||||
</Suspense>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceList = ({
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
activeSubTab,
|
||||
}: {
|
||||
onWorkspaceSettingClick: (
|
||||
subTab: WorkspaceSubTab,
|
||||
workspaceMetadata: WorkspaceMetadata
|
||||
) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
activeSubTab: WorkspaceSubTab;
|
||||
}) => {
|
||||
const workspaces = useLiveData(
|
||||
useService(WorkspacesService).list.workspaces$
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{workspaces.map(workspace => {
|
||||
return (
|
||||
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
||||
<WorkspaceListItem
|
||||
meta={workspace}
|
||||
onClick={subTab => {
|
||||
onWorkspaceSettingClick(subTab, workspace);
|
||||
}}
|
||||
activeSubTab={
|
||||
workspace.id === selectedWorkspaceId ? activeSubTab : undefined
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const subTabConfigs = [
|
||||
{
|
||||
key: 'preference',
|
||||
title: 'com.affine.settings.workspace.preferences',
|
||||
},
|
||||
{
|
||||
key: 'properties',
|
||||
title: 'com.affine.settings.workspace.properties',
|
||||
},
|
||||
] satisfies {
|
||||
key: WorkspaceSubTab;
|
||||
title: keyof ReturnType<typeof useI18n>;
|
||||
}[];
|
||||
|
||||
const WorkspaceListItem = ({
|
||||
activeSubTab,
|
||||
meta,
|
||||
onClick,
|
||||
}: {
|
||||
meta: WorkspaceMetadata;
|
||||
activeSubTab?: WorkspaceSubTab;
|
||||
onClick: (subTab: WorkspaceSubTab) => void;
|
||||
}) => {
|
||||
const { workspaceService, userFeatureService } = useServices({
|
||||
WorkspaceService,
|
||||
UserFeatureService,
|
||||
});
|
||||
const information = useWorkspaceInfo(meta);
|
||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const isCurrent = currentWorkspace.id === meta.id;
|
||||
const t = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
userFeatureService.userFeature.revalidate();
|
||||
}, [userFeatureService]);
|
||||
|
||||
const onClickPreference = useCallback(() => {
|
||||
onClick('preference');
|
||||
}, [onClick]);
|
||||
|
||||
const subTabs = useMemo(() => {
|
||||
return subTabConfigs.map(({ key, title }) => {
|
||||
return (
|
||||
<div
|
||||
data-testid={`workspace-list-item-${key}`}
|
||||
onClick={() => {
|
||||
onClick(key);
|
||||
}}
|
||||
className={clsx(style.sidebarSelectSubItem, {
|
||||
active: activeSubTab === key,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
{t[title]()}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}, [activeSubTab, onClick, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx(style.sidebarSelectItem, { active: !!activeSubTab })}
|
||||
title={name}
|
||||
onClick={onClickPreference}
|
||||
data-testid="workspace-list-item"
|
||||
>
|
||||
<WorkspaceAvatar
|
||||
key={meta.id}
|
||||
meta={meta}
|
||||
size={16}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
style={{
|
||||
marginRight: '10px',
|
||||
}}
|
||||
rounded={2}
|
||||
/>
|
||||
<span className="setting-name">{name}</span>
|
||||
{isCurrent ? (
|
||||
<Tooltip content="Current" side="top">
|
||||
<div
|
||||
className={style.currentWorkspaceLabel}
|
||||
data-testid="current-workspace-label"
|
||||
></div>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
{activeSubTab && subTabs.length > 1 ? subTabs : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const settingSlideBar = style({
|
||||
width: '25%',
|
||||
maxWidth: '242px',
|
||||
background: cssVar('backgroundSecondaryColor'),
|
||||
padding: '20px 0px',
|
||||
height: '100%',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
export const sidebarTitle = style({
|
||||
fontSize: cssVar('fontH6'),
|
||||
fontWeight: '600',
|
||||
lineHeight: cssVar('lineHeight'),
|
||||
padding: '0px 16px 0px 24px',
|
||||
});
|
||||
export const sidebarSubtitle = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: cssVar('lineHeight'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
padding: '0px 16px 0px 24px',
|
||||
marginTop: '20px',
|
||||
marginBottom: '4px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const sidebarItemsWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
selectors: {
|
||||
'&.scroll': {
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const sidebarSelectItem = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
margin: '0px 16px',
|
||||
padding: '0px 8px',
|
||||
height: '30px',
|
||||
flexShrink: 0,
|
||||
fontSize: cssVar('fontSm'),
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
':hover': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
selectors: {
|
||||
'&.active': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
export const sidebarSelectSubItem = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
margin: '0px 16px',
|
||||
padding: '0px 8px 0px 32px',
|
||||
height: '30px',
|
||||
flexShrink: 0,
|
||||
fontSize: cssVar('fontSm'),
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
selectors: {
|
||||
'&.active, &:hover': {
|
||||
color: cssVar('textPrimaryColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${settingSlideBar} .icon`, {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
marginRight: '10px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
globalStyle(`${settingSlideBar} .setting-name`, {
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flexGrow: 1,
|
||||
});
|
||||
export const currentWorkspaceLabel = style({
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
selectors: {
|
||||
'&::after': {
|
||||
content: '""',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: cssVar('blue'),
|
||||
},
|
||||
},
|
||||
});
|
||||
export const sidebarFooter = style({
|
||||
padding: '0 16px',
|
||||
});
|
||||
export const accountButton = style({
|
||||
padding: '4px 8px',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
columnGap: '10px',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
':hover': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
selectors: {
|
||||
'&.active': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${accountButton} .avatar`, {
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '50%',
|
||||
fontSize: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
});
|
||||
globalStyle(`${accountButton} .avatar.not-sign`, {
|
||||
color: cssVar('iconSecondary'),
|
||||
background: cssVar('white'),
|
||||
paddingBottom: '2px',
|
||||
border: `1px solid ${cssVar('iconSecondary')}`,
|
||||
});
|
||||
globalStyle(`${accountButton} .content`, {
|
||||
flexGrow: '1',
|
||||
minWidth: 0,
|
||||
});
|
||||
globalStyle(`${accountButton} .name-container`, {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
gap: '4px',
|
||||
height: '22px',
|
||||
});
|
||||
globalStyle(`${accountButton} .name`, {
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 600,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
height: '22px',
|
||||
});
|
||||
globalStyle(`${accountButton} .email`, {
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flexGrow: 1,
|
||||
height: '20px',
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const wrapper = style({
|
||||
flexGrow: '1',
|
||||
height: '100%',
|
||||
// margin: '0 auto',
|
||||
padding: '40px 15px 20px 15px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
export const centerContainer = style({
|
||||
width: '100%',
|
||||
maxWidth: '560px',
|
||||
});
|
||||
export const content = style({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
marginBottom: '24px',
|
||||
minHeight: 'calc(100% - 48px)',
|
||||
});
|
||||
export const suggestionLink = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
display: 'flex',
|
||||
alignItems: 'start',
|
||||
lineHeight: '22px',
|
||||
gap: '12px',
|
||||
});
|
||||
export const suggestionLinkIcon = style({
|
||||
color: cssVar('iconColor'),
|
||||
marginRight: '12px',
|
||||
display: 'flex',
|
||||
margin: '3px 0',
|
||||
});
|
||||
export const footer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingBottom: '20px',
|
||||
gap: '4px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
|
||||
export const link = style({
|
||||
color: cssVar('linkColor'),
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
export const centeredLoading = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
export const GeneralSettingKeys = [
|
||||
'shortcuts',
|
||||
'appearance',
|
||||
'about',
|
||||
'plans',
|
||||
'billing',
|
||||
'experimental-features',
|
||||
'editor',
|
||||
] as const;
|
||||
|
||||
export const WorkspaceSubTabs = ['preference', 'properties'] as const;
|
||||
|
||||
export type GeneralSettingKey = (typeof GeneralSettingKeys)[number];
|
||||
|
||||
export type WorkspaceSubTab = (typeof WorkspaceSubTabs)[number];
|
||||
|
||||
export type ActiveTab =
|
||||
| GeneralSettingKey
|
||||
| 'account'
|
||||
| `workspace:${WorkspaceSubTab}`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user