mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 02:26:21 +08:00
feat(core): open app in electron app entry (#8637)
fix PD-208 fix PD-210 fix PD-209 fix AF-1495
This commit is contained in:
@@ -6,7 +6,6 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
// Although it is called an input, it is actually a button.
|
||||
export function AppDownloadButton({
|
||||
className,
|
||||
style,
|
||||
|
||||
@@ -9,6 +9,7 @@ export const navWrapperStyle = style({
|
||||
zIndex: -1,
|
||||
},
|
||||
},
|
||||
paddingBottom: 8,
|
||||
selectors: {
|
||||
'&[data-has-border=true]': {
|
||||
borderRight: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
@@ -16,9 +17,6 @@ export const navWrapperStyle = style({
|
||||
'&[data-is-floating="true"]': {
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
},
|
||||
'&[data-client-border="true"]': {
|
||||
paddingBottom: 8,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const hoverNavWrapperStyle = style({
|
||||
|
||||
@@ -344,6 +344,7 @@ export * from './app-updater-button';
|
||||
export * from './category-divider';
|
||||
export * from './index.css';
|
||||
export * from './menu-item';
|
||||
export * from './open-in-app-card';
|
||||
export * from './quick-search-input';
|
||||
export * from './sidebar-containers';
|
||||
export * from './sidebar-header';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './open-in-app-card';
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
background: cssVarV2('layer/background/primary'),
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
export const pane = style({
|
||||
padding: '10px 12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
rowGap: 6,
|
||||
selectors: {
|
||||
'&:not(:last-of-type)': {
|
||||
borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const row = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 400,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
columnGap: 10,
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
|
||||
export const clickableRow = style([
|
||||
row,
|
||||
{
|
||||
cursor: 'pointer',
|
||||
},
|
||||
]);
|
||||
|
||||
export const buttonGroup = style({
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
});
|
||||
|
||||
export const button = style({
|
||||
height: 26,
|
||||
borderRadius: 4,
|
||||
padding: '0 8px',
|
||||
});
|
||||
|
||||
export const primaryRow = style([
|
||||
row,
|
||||
{
|
||||
color: cssVarV2('text/primary'),
|
||||
},
|
||||
]);
|
||||
|
||||
export const icon = style({
|
||||
width: 20,
|
||||
height: 20,
|
||||
flexShrink: 0,
|
||||
fontSize: 20,
|
||||
selectors: {
|
||||
[`${primaryRow} &`]: {
|
||||
color: cssVarV2('icon/activated'),
|
||||
},
|
||||
},
|
||||
});
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { Button, Checkbox } from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import {
|
||||
OpenInAppService,
|
||||
OpenLinkMode,
|
||||
} from '@affine/core/modules/open-in-app';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { DownloadIcon, LocalWorkspaceIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import * as styles from './open-in-app-card.css';
|
||||
|
||||
export const OpenInAppCard = ({ className }: { className?: string }) => {
|
||||
const openInAppService = useService(OpenInAppService);
|
||||
const show = useLiveData(openInAppService.showOpenInAppBanner$);
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const t = useI18n();
|
||||
|
||||
const [remember, setRemember] = useState(false);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
navigateHelper.jumpToOpenInApp(window.location.href, true);
|
||||
if (remember) {
|
||||
openInAppService.setOpenLinkMode(OpenLinkMode.OPEN_IN_DESKTOP_APP);
|
||||
}
|
||||
}, [openInAppService, remember, navigateHelper]);
|
||||
|
||||
const onDismiss = useCallback(() => {
|
||||
openInAppService.dismissBanner(
|
||||
remember ? OpenLinkMode.OPEN_IN_WEB : undefined
|
||||
);
|
||||
}, [openInAppService, remember]);
|
||||
|
||||
const onToggleRemember = useCallback(() => {
|
||||
setRemember(v => !v);
|
||||
}, []);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
track.$.navigationPanel.bottomButtons.downloadApp();
|
||||
const url = `https://affine.pro/download?channel=stable`;
|
||||
open(url, '_blank');
|
||||
}, []);
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.root, className)}>
|
||||
<div className={styles.pane}>
|
||||
<div className={styles.primaryRow}>
|
||||
<LocalWorkspaceIcon className={styles.icon} />
|
||||
<div>{t.t('com.affine.open-in-app.card.title')}</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.icon}>{/* placeholder */}</div>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="custom"
|
||||
className={styles.button}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{t.t('com.affine.open-in-app.card.button.open')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="custom"
|
||||
className={styles.button}
|
||||
onClick={onDismiss}
|
||||
>
|
||||
{t.t('com.affine.open-in-app.card.button.dismiss')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.pane}>
|
||||
<div className={styles.clickableRow} onClick={onToggleRemember}>
|
||||
<Checkbox className={styles.icon} checked={remember} />
|
||||
<div>{t.t('com.affine.open-in-app.card.remember')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.pane}>
|
||||
<div className={styles.clickableRow} onClick={handleDownload}>
|
||||
<DownloadIcon className={styles.icon} />
|
||||
<div>{t.t('com.affine.open-in-app.card.download')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { afterEach } from 'node:test';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { channelToScheme } from '@affine/core/utils';
|
||||
import type { ReferenceParams } from '@blocksuite/affine/blocks';
|
||||
import { isNil, pick, pickBy } from 'lodash-es';
|
||||
import type { ParsedQuery, ParseOptions } from 'query-string';
|
||||
@@ -6,7 +7,6 @@ import queryString from 'query-string';
|
||||
function maybeAffineOrigin(origin: string, baseUrl: string) {
|
||||
return (
|
||||
origin.startsWith('file://') ||
|
||||
origin.startsWith('affine://') ||
|
||||
origin.endsWith('affine.pro') || // stable/beta
|
||||
origin.endsWith('affine.fail') || // canary
|
||||
origin === baseUrl // localhost or self-hosted
|
||||
@@ -18,6 +18,13 @@ export const resolveRouteLinkMeta = (
|
||||
baseUrl = location.origin
|
||||
) => {
|
||||
try {
|
||||
// if href is started with affine protocol, we need to convert it to http protocol to may URL happy
|
||||
const affineProtocol = channelToScheme[BUILD_CONFIG.appBuildType] + '://';
|
||||
|
||||
if (href.startsWith(affineProtocol)) {
|
||||
href = href.replace(affineProtocol, 'http://');
|
||||
}
|
||||
|
||||
const url = new URL(href, baseUrl);
|
||||
|
||||
// check if origin is one of affine's origins
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const appSchemes = z.enum([
|
||||
'affine',
|
||||
'affine-canary',
|
||||
'affine-beta',
|
||||
'affine-internal',
|
||||
'affine-dev',
|
||||
]);
|
||||
|
||||
const appChannelSchemes = z.enum(['stable', 'canary', 'beta', 'internal']);
|
||||
|
||||
export type Scheme = z.infer<typeof appSchemes>;
|
||||
export type Channel = z.infer<typeof appChannelSchemes>;
|
||||
|
||||
export const schemeToChannel = {
|
||||
affine: 'stable',
|
||||
'affine-canary': 'canary',
|
||||
'affine-beta': 'beta',
|
||||
'affine-internal': 'internal',
|
||||
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
|
||||
} as Record<Scheme, Channel>;
|
||||
|
||||
export const channelToScheme = {
|
||||
stable: 'affine',
|
||||
canary:
|
||||
process.env.NODE_ENV === 'development' ? 'affine-dev' : 'affine-canary',
|
||||
beta: 'affine-beta',
|
||||
internal: 'affine-internal',
|
||||
} as Record<Channel, Scheme>;
|
||||
|
||||
export const appIconMap = {
|
||||
stable: '/imgs/app-icon-stable.ico',
|
||||
canary: '/imgs/app-icon-canary.ico',
|
||||
beta: '/imgs/app-icon-beta.ico',
|
||||
internal: '/imgs/app-icon-internal.ico',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
export const appNames = {
|
||||
stable: 'AFFiNE',
|
||||
canary: 'AFFiNE Canary',
|
||||
beta: 'AFFiNE Beta',
|
||||
internal: 'AFFiNE Internal',
|
||||
} satisfies Record<Channel, string>;
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
type Framework,
|
||||
GlobalState,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { OpenInAppService } from './services';
|
||||
|
||||
export * from './services';
|
||||
export * from './utils';
|
||||
|
||||
export const configureOpenInApp = (framework: Framework) => {
|
||||
framework.service(OpenInAppService, [GlobalState, WorkspacesService]);
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { GlobalState, WorkspacesService } from '@toeverything/infra';
|
||||
import { LiveData, OnEvent, Service } from '@toeverything/infra';
|
||||
|
||||
import { resolveLinkToDoc } from '../../navigation';
|
||||
import { WorkbenchLocationChanged } from '../../workbench/services/workbench';
|
||||
import { getLocalWorkspaceIds } from '../../workspace-engine/impls/local';
|
||||
|
||||
const storageKey = 'open-link-mode';
|
||||
|
||||
export enum OpenLinkMode {
|
||||
ALWAYS_ASK = 'always-ask', // default
|
||||
OPEN_IN_WEB = 'open-in-web',
|
||||
OPEN_IN_DESKTOP_APP = 'open-in-desktop-app',
|
||||
}
|
||||
|
||||
@OnEvent(WorkbenchLocationChanged, e => e.onNavigation)
|
||||
export class OpenInAppService extends Service {
|
||||
private initialized = false;
|
||||
|
||||
private initialUrl: string | undefined;
|
||||
|
||||
readonly showOpenInAppBanner$ = new LiveData<boolean>(false);
|
||||
readonly showOpenInAppPage$ = new LiveData<boolean | undefined>(undefined);
|
||||
|
||||
constructor(
|
||||
public readonly globalState: GlobalState,
|
||||
public readonly workspacesService: WorkspacesService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
onNavigation() {
|
||||
// check doc id instead?
|
||||
if (window.location.href === this.initialUrl) {
|
||||
return;
|
||||
}
|
||||
this.showOpenInAppBanner$.next(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the initial URL, check if we need to redirect to the desktop app.
|
||||
*/
|
||||
bootstrap() {
|
||||
if (this.initialized || !window) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
this.initialUrl = window.location.href;
|
||||
|
||||
const maybeDocLink = resolveLinkToDoc(this.initialUrl);
|
||||
let shouldOpenInApp = false;
|
||||
|
||||
const localWorkspaceIds = getLocalWorkspaceIds();
|
||||
|
||||
if (maybeDocLink && !localWorkspaceIds.includes(maybeDocLink.workspaceId)) {
|
||||
switch (this.getOpenLinkMode()) {
|
||||
case OpenLinkMode.OPEN_IN_DESKTOP_APP:
|
||||
shouldOpenInApp = true;
|
||||
break;
|
||||
case OpenLinkMode.ALWAYS_ASK:
|
||||
this.showOpenInAppBanner$.next(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.showOpenInAppPage$.next(shouldOpenInApp);
|
||||
}
|
||||
|
||||
showOpenInAppPage() {
|
||||
this.showOpenInAppPage$.next(true);
|
||||
}
|
||||
|
||||
hideOpenInAppPage() {
|
||||
this.showOpenInAppPage$.next(false);
|
||||
}
|
||||
|
||||
getOpenLinkMode() {
|
||||
return (
|
||||
this.globalState.get<OpenLinkMode>(storageKey) ?? OpenLinkMode.ALWAYS_ASK
|
||||
);
|
||||
}
|
||||
|
||||
openLinkMode$ = LiveData.from(
|
||||
this.globalState.watch<OpenLinkMode>(storageKey),
|
||||
this.getOpenLinkMode()
|
||||
).map(v => v ?? OpenLinkMode.ALWAYS_ASK);
|
||||
|
||||
setOpenLinkMode(mode: OpenLinkMode) {
|
||||
this.globalState.set(storageKey, mode);
|
||||
}
|
||||
|
||||
dismissBanner(rememberMode: OpenLinkMode | undefined) {
|
||||
if (rememberMode) {
|
||||
this.globalState.set(storageKey, rememberMode);
|
||||
}
|
||||
this.showOpenInAppBanner$.next(false);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
import { channelToScheme } from '@affine/core/utils';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
import { channelToScheme } from './constant';
|
||||
|
||||
const logger = new DebugLogger('open-in-app');
|
||||
|
||||
// return an AFFiNE app's url to be opened in desktop app
|
||||
export const getOpenUrlInDesktopAppLink = (
|
||||
url: string,
|
||||
newTab = false,
|
||||
newTab = true,
|
||||
scheme = channelToScheme[BUILD_CONFIG.appBuildType]
|
||||
) => {
|
||||
if (!scheme) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlObject = new URL(url);
|
||||
const params = urlObject.searchParams;
|
||||
|
||||
if (newTab) {
|
||||
params.set('new-tab', '1');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!scheme) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlObject = new URL(url, location.origin);
|
||||
const params = urlObject.searchParams;
|
||||
|
||||
if (newTab) {
|
||||
params.set('new-tab', '1');
|
||||
}
|
||||
return new URL(
|
||||
`${scheme}://${urlObject.host}${urlObject.pathname}?${params.toString()}#${urlObject.hash}`
|
||||
).toString();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { OpenInAppService } from '../services';
|
||||
import { OpenInAppPage } from './open-in-app-page';
|
||||
|
||||
/**
|
||||
* Web only guard to open the URL in desktop app for different conditions
|
||||
*/
|
||||
export const WebOpenInAppGuard = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
assertExists(
|
||||
BUILD_CONFIG.isWeb,
|
||||
'WebOpenInAppGuard should only be used in web'
|
||||
);
|
||||
const service = useService(OpenInAppService);
|
||||
const shouldOpenInApp = useLiveData(service.showOpenInAppPage$);
|
||||
|
||||
useEffect(() => {
|
||||
service?.bootstrap();
|
||||
}, [service]);
|
||||
|
||||
const onOpenHere = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
service.hideOpenInAppPage();
|
||||
},
|
||||
[service]
|
||||
);
|
||||
|
||||
if (shouldOpenInApp === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return shouldOpenInApp ? (
|
||||
<OpenInAppPage openHereClicked={onOpenHere} />
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: cssVar('fontBase'),
|
||||
position: 'relative',
|
||||
});
|
||||
export const affineLogo = style({
|
||||
color: 'inherit',
|
||||
});
|
||||
export const topNav = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 120px',
|
||||
});
|
||||
export const topNavLinks = style({
|
||||
display: 'flex',
|
||||
columnGap: 4,
|
||||
});
|
||||
export const topNavLink = style({
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
padding: '4px 18px',
|
||||
});
|
||||
|
||||
export const promptLinks = style({
|
||||
display: 'flex',
|
||||
columnGap: 16,
|
||||
});
|
||||
export const promptLink = style({
|
||||
color: cssVar('linkColor'),
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
fontSize: cssVar('fontSm'),
|
||||
});
|
||||
export const centerContent = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: 40,
|
||||
});
|
||||
export const prompt = style({
|
||||
marginTop: 20,
|
||||
marginBottom: 12,
|
||||
});
|
||||
export const editSettingsLink = style({
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
color: cssVar('linkColor'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
position: 'absolute',
|
||||
bottom: 48,
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { openSettingModalAtom } from '@affine/core/components/atoms';
|
||||
import { resolveLinkToDoc } from '@affine/core/modules/navigation';
|
||||
import { appIconMap, appNames } from '@affine/core/utils';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { Logo1Icon } from '@blocksuite/icons/rc';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getOpenUrlInDesktopAppLink } from '../utils';
|
||||
import * as styles from './open-in-app-page.css';
|
||||
|
||||
let lastOpened = '';
|
||||
|
||||
interface OpenAppProps {
|
||||
urlToOpen?: string | null;
|
||||
openHereClicked?: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const OpenInAppPage = ({ urlToOpen, openHereClicked }: OpenAppProps) => {
|
||||
// default to open the current page in desktop app
|
||||
urlToOpen ??= getOpenUrlInDesktopAppLink(window.location.href, true);
|
||||
const t = useI18n();
|
||||
const channel = BUILD_CONFIG.appBuildType;
|
||||
const openDownloadLink = useCallback(() => {
|
||||
const url =
|
||||
'https://affine.pro/download' +
|
||||
(channel !== 'stable' ? '/beta-canary' : '');
|
||||
open(url, '_blank');
|
||||
}, [channel]);
|
||||
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
|
||||
const maybeDocLink = urlToOpen ? resolveLinkToDoc(urlToOpen) : null;
|
||||
|
||||
const goToDocPage = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!maybeDocLink) {
|
||||
return;
|
||||
}
|
||||
openHereClicked?.(e);
|
||||
},
|
||||
[maybeDocLink, openHereClicked]
|
||||
);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const goToAppearanceSetting = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
openHereClicked?.(e);
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'appearance',
|
||||
});
|
||||
},
|
||||
[openHereClicked, setSettingModalAtom]
|
||||
);
|
||||
|
||||
if (urlToOpen && lastOpened !== urlToOpen) {
|
||||
lastOpened = urlToOpen;
|
||||
location.href = urlToOpen;
|
||||
}
|
||||
|
||||
if (!urlToOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.topNav}>
|
||||
<a href="/" rel="noreferrer" className={styles.affineLogo}>
|
||||
<Logo1Icon width={24} height={24} />
|
||||
</a>
|
||||
|
||||
<div className={styles.topNavLinks}>
|
||||
<a
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Official Website
|
||||
</a>
|
||||
<a
|
||||
href="https://community.affine.pro/home"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
AFFiNE Community
|
||||
</a>
|
||||
<a
|
||||
href="https://affine.pro/blog"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://affine.pro/about-us"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Contact us
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button onClick={openDownloadLink}>
|
||||
{t['com.affine.auth.open.affine.download-app']()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.centerContent}>
|
||||
<img src={appIcon} alt={appName} width={120} height={120} />
|
||||
|
||||
<div className={styles.prompt}>
|
||||
<Trans i18nKey="com.affine.auth.open.affine.prompt">
|
||||
Open {appName} app now
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<div className={styles.promptLinks}>
|
||||
{openHereClicked && (
|
||||
<a
|
||||
className={styles.promptLink}
|
||||
onClick={goToDocPage}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.auth.open.affine.doc.open-here']()}
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
className={styles.promptLink}
|
||||
href={urlToOpen}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.auth.open.affine.try-again']()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{maybeDocLink ? (
|
||||
<a
|
||||
className={styles.editSettingsLink}
|
||||
onClick={goToAppearanceSetting}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.auth.open.affine.doc.edit-settings']()}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user