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:
pengx17
2024-10-31 06:16:32 +00:00
parent 5d92c900d1
commit 0f8b273134
36 changed files with 887 additions and 226 deletions

View File

@@ -6,10 +6,10 @@ import { buildType, isDev } from './config';
import { logger } from './logger'; import { logger } from './logger';
import { uiSubjects } from './ui'; import { uiSubjects } from './ui';
import { import {
addTab, addTabWithUrl,
getMainWindow, getMainWindow,
loadUrlInActiveTab,
openUrlInHiddenWindow, openUrlInHiddenWindow,
openUrlInMainWindow,
showMainWindow, showMainWindow,
} from './windows-manager'; } from './windows-manager';
@@ -88,22 +88,11 @@ async function handleAffineUrl(url: string) {
) { ) {
// @todo(@forehalo): refactor router utilities // @todo(@forehalo): refactor router utilities
// basename of /workspace/xxx/yyy is /workspace/xxx // basename of /workspace/xxx/yyy is /workspace/xxx
const basename = urlObj.pathname.split('/').slice(0, 3).join('/'); await addTabWithUrl(url);
const pathname = '/' + urlObj.pathname.split('/').slice(3).join('/');
await addTab({
basename,
show: true,
view: {
path: {
pathname: pathname,
},
},
});
} else { } else {
const hiddenWindow = urlObj.searchParams.get('hidden') const hiddenWindow = urlObj.searchParams.get('hidden')
? await openUrlInHiddenWindow(urlObj) ? await openUrlInHiddenWindow(urlObj)
: await openUrlInMainWindow(urlObj); : await loadUrlInActiveTab(url);
const main = await getMainWindow(); const main = await getMainWindow();
if (main && hiddenWindow) { if (main && hiddenWindow) {

View File

@@ -273,15 +273,3 @@ export async function openUrlInHiddenWindow(urlObj: URL) {
}); });
return win; return win;
} }
// TODO(@pengx17): somehow the page won't load the url passed, help needed
export async function openUrlInMainWindow(urlObj: URL) {
const url = transformToAppUrl(urlObj);
logger.info('loading page at', url);
const mainWindow = await getMainWindow();
if (mainWindow) {
await mainWindow.loadURL(url);
}
return null;
}

View File

@@ -932,7 +932,35 @@ export const isActiveTab = (wc: WebContents) => {
WebContentViewsManager.instance.activeWorkbenchView?.webContents.id WebContentViewsManager.instance.activeWorkbenchView?.webContents.id
); );
}; };
// parse the full pathname to basename and pathname
// eg: /workspace/xxx/yyy => { basename: '/workspace/xxx', pathname: '/yyy' }
export const parseFullPathname = (url: string) => {
const urlObj = new URL(url);
const basename = urlObj.pathname.match(/\/workspace\/[^/]+/g)?.[0] ?? '/';
return {
basename,
pathname: urlObj.pathname.slice(basename.length),
search: urlObj.search,
hash: urlObj.hash,
};
};
export const addTab = WebContentViewsManager.instance.addTab; export const addTab = WebContentViewsManager.instance.addTab;
export const addTabWithUrl = (url: string) => {
const { basename, pathname, search, hash } = parseFullPathname(url);
return addTab({
basename,
view: {
path: { pathname, search, hash },
},
});
};
export const loadUrlInActiveTab = async (_url: string) => {
// todo: implement
throw new Error('loadUrlInActiveTab not implemented');
};
export const showTab = WebContentViewsManager.instance.showTab; export const showTab = WebContentViewsManager.instance.showTab;
export const closeTab = WebContentViewsManager.instance.closeTab; export const closeTab = WebContentViewsManager.instance.closeTab;
export const undoCloseTab = WebContentViewsManager.instance.undoCloseTab; export const undoCloseTab = WebContentViewsManager.instance.undoCloseTab;

View File

@@ -5,6 +5,8 @@ import { Telemetry } from '@affine/core/components/telemetry';
import { router } from '@affine/core/desktop/router'; import { router } from '@affine/core/desktop/router';
import { configureCommonModules } from '@affine/core/modules'; import { configureCommonModules } from '@affine/core/modules';
import { I18nProvider } from '@affine/core/modules/i18n'; import { I18nProvider } from '@affine/core/modules/i18n';
import { configureOpenInApp } from '@affine/core/modules/open-in-app';
import { WebOpenInAppGuard } from '@affine/core/modules/open-in-app/views/open-in-app-guard';
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage'; import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
import { CustomThemeModifier } from '@affine/core/modules/theme-editor'; import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
import { PopupWindowProvider } from '@affine/core/modules/url'; import { PopupWindowProvider } from '@affine/core/modules/url';
@@ -38,6 +40,7 @@ configureLocalStorageStateStorageImpls(framework);
configureBrowserWorkspaceFlavours(framework); configureBrowserWorkspaceFlavours(framework);
configureIndexedDBWorkspaceEngineStorageProvider(framework); configureIndexedDBWorkspaceEngineStorageProvider(framework);
configureIndexedDBUserspaceStorageProvider(framework); configureIndexedDBUserspaceStorageProvider(framework);
configureOpenInApp(framework);
framework.impl(PopupWindowProvider, { framework.impl(PopupWindowProvider, {
open: (target: string) => { open: (target: string) => {
const targetUrl = new URL(target); const targetUrl = new URL(target);
@@ -75,11 +78,13 @@ export function App() {
<Telemetry /> <Telemetry />
<CustomThemeModifier /> <CustomThemeModifier />
<GlobalLoading /> <GlobalLoading />
<RouterProvider <WebOpenInAppGuard>
fallbackElement={<AppFallback key="RouterFallback" />} <RouterProvider
router={router} fallbackElement={<AppFallback key="RouterFallback" />}
future={future} router={router}
/> future={future}
/>
</WebOpenInAppGuard>
</AffineContext> </AffineContext>
</I18nProvider> </I18nProvider>
</CacheProvider> </CacheProvider>

View File

@@ -11,7 +11,7 @@ export type CheckboxProps = Omit<
'onChange' 'onChange'
> & { > & {
checked: boolean; checked: boolean;
onChange: ( onChange?: (
event: React.ChangeEvent<HTMLInputElement>, event: React.ChangeEvent<HTMLInputElement>,
checked: boolean checked: boolean
) => void; ) => void;
@@ -41,7 +41,7 @@ export const Checkbox = ({
const handleChange = useCallback( const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => { (event: React.ChangeEvent<HTMLInputElement>) => {
const newChecked = event.target.checked; const newChecked = event.target.checked;
onChange(event, newChecked); onChange?.(event, newChecked);
const inputElement = inputRef.current; const inputElement = inputRef.current;
if (newChecked && inputElement && animation) { if (newChecked && inputElement && animation) {
playCheckAnimation(inputElement.parentElement as Element).catch( playCheckAnimation(inputElement.parentElement as Element).catch(

View File

@@ -5,11 +5,8 @@ import {
SettingWrapper, SettingWrapper,
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { useAppUpdater } from '@affine/core/components/hooks/use-app-updater'; import { useAppUpdater } from '@affine/core/components/hooks/use-app-updater';
import {
appIconMap,
appNames,
} from '@affine/core/modules/open-in-app/constant';
import { UrlService } from '@affine/core/modules/url'; import { UrlService } from '@affine/core/modules/url';
import { appIconMap, appNames } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { mixpanel } from '@affine/track'; import { mixpanel } from '@affine/track';
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons/rc'; import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons/rc';

View File

@@ -16,6 +16,7 @@ import { useCallback, useMemo } from 'react';
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
import { LanguageMenu } from '../../../language-menu'; import { LanguageMenu } from '../../../language-menu';
import { OpenInAppLinksMenu } from './links';
import { settingWrapper } from './style.css'; import { settingWrapper } from './style.css';
import { ThemeEditorSetting } from './theme-editor-setting'; import { ThemeEditorSetting } from './theme-editor-setting';
@@ -106,6 +107,17 @@ export const AppearanceSettings = () => {
{enableThemeEditor ? <ThemeEditorSetting /> : null} {enableThemeEditor ? <ThemeEditorSetting /> : null}
</SettingWrapper> </SettingWrapper>
{BUILD_CONFIG.isWeb ? (
<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 ? ( {BUILD_CONFIG.isElectron ? (
<SettingWrapper <SettingWrapper
title={t['com.affine.appearanceSettings.sidebar.title']()} title={t['com.affine.appearanceSettings.sidebar.title']()}

View File

@@ -0,0 +1,18 @@
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'),
},
},
});

View File

@@ -0,0 +1,55 @@
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={{ textTransform: 'capitalize', fontWeight: 600, width: '250px' }}
block={true}
>
{options.find(option => option.value === currentOpenInAppMode)?.label}
</MenuTrigger>
</Menu>
);
};

View File

@@ -18,14 +18,14 @@ const UpgradeSuccessLayout = ({
const t = useI18n(); const t = useI18n();
const [params] = useSearchParams(); const [params] = useSearchParams();
const { jumpToIndex, openInApp } = useNavigateHelper(); const { jumpToIndex, jumpToOpenInApp } = useNavigateHelper();
const openAffine = useCallback(() => { const openAffine = useCallback(() => {
if (params.get('scheme')) { if (params.get('scheme')) {
openInApp(params.get('scheme') ?? 'affine', 'bring-to-front'); jumpToOpenInApp('bring-to-front');
} else { } else {
jumpToIndex(); jumpToIndex();
} }
}, [jumpToIndex, openInApp, params]); }, [jumpToIndex, jumpToOpenInApp, params]);
const subtitle = ( const subtitle = (
<div className={styles.leftContentText}> <div className={styles.leftContentText}>

View File

@@ -25,7 +25,7 @@ import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive'; import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
import { DocInfoService } from '@affine/core/modules/doc-info'; import { DocInfoService } from '@affine/core/modules/doc-info';
import { EditorService } from '@affine/core/modules/editor'; import { EditorService } from '@affine/core/modules/editor';
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app/utils'; import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewService } from '@affine/core/modules/workbench/services/view'; import { ViewService } from '@affine/core/modules/workbench/services/view';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
@@ -52,6 +52,7 @@ import {
FeatureFlagService, FeatureFlagService,
useLiveData, useLiveData,
useService, useService,
useServiceOptional,
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
@@ -92,6 +93,7 @@ export const PageHeaderMenuButton = ({
const enableSnapshotImportExport = useLiveData( const enableSnapshotImportExport = useLiveData(
featureFlagService.flags.enable_snapshot_import_export.$ featureFlagService.flags.enable_snapshot_import_export.$
); );
const openInAppService = useServiceOptional(OpenInAppService);
const { favorite, toggleFavorite } = useFavorite(pageId); const { favorite, toggleFavorite } = useFavorite(pageId);
@@ -265,11 +267,8 @@ export const PageHeaderMenuButton = ({
); );
const onOpenInDesktop = useCallback(() => { const onOpenInDesktop = useCallback(() => {
const url = getOpenUrlInDesktopAppLink(window.location.href, true); openInAppService?.showOpenInAppPage();
if (url) { }, [openInAppService]);
window.open(url, '_blank');
}
}, []);
const EditMenu = ( const EditMenu = (
<> <>
@@ -376,7 +375,8 @@ export const PageHeaderMenuButton = ({
data-testid="editor-option-menu-delete" data-testid="editor-option-menu-delete"
onSelect={handleOpenTrashModal} onSelect={handleOpenTrashModal}
/> />
{BUILD_CONFIG.isWeb ? ( {BUILD_CONFIG.isWeb &&
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
<MenuItem <MenuItem
prefixIcon={<LocalWorkspaceIcon />} prefixIcon={<LocalWorkspaceIcon />}
data-testid="editor-option-menu-link" data-testid="editor-option-menu-link"

View File

@@ -1,4 +1,5 @@
import { toURLSearchParams } from '@affine/core/modules/navigation'; import { toURLSearchParams } from '@affine/core/modules/navigation';
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app';
import type { DocMode } from '@blocksuite/affine/blocks'; import type { DocMode } from '@blocksuite/affine/blocks';
import { createContext, useCallback, useContext, useMemo } from 'react'; import { createContext, useCallback, useContext, useMemo } from 'react';
import type { NavigateFunction, NavigateOptions } from 'react-router-dom'; import type { NavigateFunction, NavigateOptions } from 'react-router-dom';
@@ -159,10 +160,16 @@ export function useNavigateHelper() {
[navigate] [navigate]
); );
const openInApp = useCallback( const jumpToOpenInApp = useCallback(
(scheme: string, path: string) => { (url: string, newTab = true) => {
const encodedUrl = encodeURIComponent(`${scheme}://${path}`); const deeplink = getOpenUrlInDesktopAppLink(url, newTab);
return navigate(`/open-app/url?scheme=${scheme}&url=${encodedUrl}`);
if (!deeplink) {
return;
}
const encodedUrl = encodeURIComponent(deeplink);
return navigate(`/open-app/url?url=${encodedUrl}`);
}, },
[navigate] [navigate]
); );
@@ -189,7 +196,7 @@ export function useNavigateHelper() {
jumpToCollections, jumpToCollections,
jumpToTags, jumpToTags,
jumpToTag, jumpToTag,
openInApp, jumpToOpenInApp,
jumpToImportTemplate, jumpToImportTemplate,
}), }),
[ [
@@ -204,7 +211,7 @@ export function useNavigateHelper() {
jumpToCollections, jumpToCollections,
jumpToTags, jumpToTags,
jumpToTag, jumpToTag,
openInApp, jumpToOpenInApp,
jumpToImportTemplate, jumpToImportTemplate,
] ]
); );

View File

@@ -5,11 +5,11 @@ import {
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { import {
AddPageButton, AddPageButton,
AppDownloadButton,
AppSidebar, AppSidebar,
CategoryDivider, CategoryDivider,
MenuItem, MenuItem,
MenuLinkItem, MenuLinkItem,
OpenInAppCard,
QuickSearchInput, QuickSearchInput,
SidebarContainer, SidebarContainer,
SidebarScrollableContainer, SidebarScrollableContainer,
@@ -190,7 +190,7 @@ export const RootAppSidebar = (): ReactElement => {
</div> </div>
</SidebarScrollableContainer> </SidebarScrollableContainer>
<SidebarContainer> <SidebarContainer>
{BUILD_CONFIG.isElectron ? <UpdaterButton /> : <AppDownloadButton />} {BUILD_CONFIG.isElectron ? <UpdaterButton /> : <OpenInAppCard />}
</SidebarContainer> </SidebarContainer>
</AppSidebar> </AppSidebar>
); );

View File

@@ -33,7 +33,12 @@ export const topNavLink = style({
textDecoration: 'none', textDecoration: 'none',
padding: '4px 18px', padding: '4px 18px',
}); });
export const tryAgainLink = style({
export const promptLinks = style({
display: 'flex',
columnGap: 16,
});
export const promptLink = style({
color: cssVar('linkColor'), color: cssVar('linkColor'),
fontWeight: 500, fontWeight: 500,
textDecoration: 'none', textDecoration: 'none',
@@ -49,3 +54,11 @@ export const prompt = style({
marginTop: 20, marginTop: 20,
marginBottom: 12, marginBottom: 12,
}); });
export const editSettingsLink = style({
fontWeight: 500,
textDecoration: 'none',
color: cssVar('linkColor'),
fontSize: cssVar('fontSm'),
position: 'absolute',
bottom: 48,
});

View File

@@ -1,134 +1,32 @@
import { Button } from '@affine/component/ui/button'; import { OpenInAppPage } from '@affine/core/modules/open-in-app/views/open-in-app-page';
import { import { appSchemes } from '@affine/core/utils';
appIconMap,
appNames,
appSchemes,
type Channel,
schemeToChannel,
} from '@affine/core/modules/open-in-app/constant';
import type { GetCurrentUserQuery } from '@affine/graphql'; import type { GetCurrentUserQuery } from '@affine/graphql';
import { fetcher, getCurrentUserQuery } from '@affine/graphql'; import { fetcher, getCurrentUserQuery } from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n';
import { Logo1Icon } from '@blocksuite/icons/rc';
import { useCallback } from 'react';
import type { LoaderFunction } from 'react-router-dom'; import type { LoaderFunction } from 'react-router-dom';
import { useLoaderData, useSearchParams } from 'react-router-dom'; import { useLoaderData, useSearchParams } from 'react-router-dom';
import * as styles from './open-app.css';
let lastOpened = '';
interface OpenAppProps {
urlToOpen?: string | null;
channel: Channel;
}
interface LoaderData { interface LoaderData {
action: 'url' | 'signin-redirect'; action: 'url' | 'signin-redirect';
currentUser?: GetCurrentUserQuery['currentUser']; currentUser?: GetCurrentUserQuery['currentUser'];
} }
const OpenAppImpl = ({ urlToOpen, channel }: OpenAppProps) => { const OpenUrl = () => {
const t = useI18n(); const [params] = useSearchParams();
const openDownloadLink = useCallback(() => { const urlToOpen = params.get('url');
const url = `https://affine.pro/download?channel=${channel}`;
open(url, '_blank');
}, [channel]);
const appIcon = appIconMap[channel];
const appName = appNames[channel];
if (urlToOpen && lastOpened !== urlToOpen) {
lastOpened = urlToOpen;
location.href = urlToOpen;
}
if (!urlToOpen) { if (!urlToOpen) {
return null; 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>
<a
className={styles.tryAgainLink}
href={urlToOpen}
target="_blank"
rel="noreferrer"
>
{t['com.affine.auth.open.affine.try-again']()}
</a>
</div>
</div>
);
};
const OpenUrl = () => {
const [params] = useSearchParams();
const urlToOpen = params.get('url');
params.delete('url'); params.delete('url');
const urlObj = new URL(urlToOpen || ''); const urlObj = new URL(urlToOpen || '');
const maybeScheme = appSchemes.safeParse(urlObj.protocol.replace(':', ''));
const channel =
schemeToChannel[maybeScheme.success ? maybeScheme.data : 'affine'];
params.forEach((v, k) => { params.forEach((v, k) => {
urlObj.searchParams.set(k, v); urlObj.searchParams.set(k, v);
}); });
return <OpenAppImpl urlToOpen={urlObj.toString()} channel={channel} />; return <OpenInAppPage urlToOpen={urlObj.toString()} />;
}; };
/** /**
@@ -141,7 +39,6 @@ const OpenOAuthJwt = () => {
const maybeScheme = appSchemes.safeParse(params.get('scheme')); const maybeScheme = appSchemes.safeParse(params.get('scheme'));
const scheme = maybeScheme.success ? maybeScheme.data : 'affine'; const scheme = maybeScheme.success ? maybeScheme.data : 'affine';
const next = params.get('next'); const next = params.get('next');
const channel = schemeToChannel[scheme];
if (!currentUser || !currentUser?.token?.sessionToken) { if (!currentUser || !currentUser?.token?.sessionToken) {
return null; return null;
@@ -151,7 +48,7 @@ const OpenOAuthJwt = () => {
currentUser.token.sessionToken currentUser.token.sessionToken
}&next=${next || ''}`; }&next=${next || ''}`;
return <OpenAppImpl urlToOpen={urlToOpen} channel={channel} />; return <OpenInAppPage urlToOpen={urlToOpen} />;
}; };
export const Component = () => { export const Component = () => {

View File

@@ -97,10 +97,7 @@ export const Component = (): ReactElement => {
}, [listLoading, meta, workspaceNotFound, workspacesService]); }, [listLoading, meta, workspaceNotFound, workspacesService]);
if (workspaceNotFound) { if (workspaceNotFound) {
if ( if (detailDocRoute) {
!BUILD_CONFIG.isElectron /* only browser has share page */ &&
detailDocRoute
) {
return ( return (
<SharePage <SharePage
docId={detailDocRoute.docId} docId={detailDocRoute.docId}

View File

@@ -6,6 +6,7 @@ export const root = style({
height: '100%', height: '100%',
overflow: 'hidden', overflow: 'hidden',
width: '100%', width: '100%',
flexDirection: 'column',
}); });
export const mainContainer = style({ export const mainContainer = style({
@@ -56,3 +57,9 @@ export const linkText = style({
fontWeight: 700, fontWeight: 700,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}); });
export const shareCard = style({
position: 'fixed',
bottom: '16px',
left: '16px',
});

View File

@@ -7,6 +7,8 @@ import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-he
import { PageDetailEditor } from '@affine/core/components/page-detail-editor'; import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
import { SharePageNotFoundError } from '@affine/core/components/share-page-not-found-error'; import { SharePageNotFoundError } from '@affine/core/components/share-page-not-found-error';
import { AppContainer, MainContainer } from '@affine/core/components/workspace'; import { AppContainer, MainContainer } from '@affine/core/components/workspace';
import { OpenInAppCard } from '@affine/core/modules/app-sidebar/views';
import { AppTabsHeader } from '@affine/core/modules/app-tabs-header';
import { AuthService, FetchService } from '@affine/core/modules/cloud'; import { AuthService, FetchService } from '@affine/core/modules/cloud';
import { import {
type Editor, type Editor,
@@ -16,6 +18,8 @@ import {
} from '@affine/core/modules/editor'; } from '@affine/core/modules/editor';
import { PeekViewManagerModal } from '@affine/core/modules/peek-view'; import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
import { ShareReaderService } from '@affine/core/modules/share-doc'; import { ShareReaderService } from '@affine/core/modules/share-doc';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import { DesktopStateSynchronizer } from '@affine/core/modules/workbench/services/desktop-state-synchronizer';
import { CloudBlobStorage } from '@affine/core/modules/workspace-engine'; import { CloudBlobStorage } from '@affine/core/modules/workspace-engine';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@@ -35,11 +39,18 @@ import {
ReadonlyDocStorage, ReadonlyDocStorage,
useLiveData, useLiveData,
useService, useService,
useServiceOptional,
useServices, useServices,
WorkspacesService, WorkspacesService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import {
type PropsWithChildren,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
@@ -125,6 +136,75 @@ export const SharePage = ({
} }
}; };
interface SharePageContainerProps {
pageId: string;
pageTitle?: string;
publishMode: DocMode;
isTemplate?: boolean;
templateName?: string;
templateSnapshotUrl?: string;
}
const SharePageWebContainer = ({
children,
pageId,
publishMode,
isTemplate,
templateName,
templateSnapshotUrl,
}: PropsWithChildren<SharePageContainerProps>) => {
return (
<MainContainer>
<div className={styles.root}>
<div className={styles.mainContainer}>
<ShareHeader
pageId={pageId}
publishMode={publishMode}
isTemplate={isTemplate}
templateName={templateName}
snapshotUrl={templateSnapshotUrl}
/>
{children}
<SharePageFooter />
</div>
<OpenInAppCard className={styles.shareCard} />
</div>
</MainContainer>
);
};
const SharePageDesktopContainer = ({
children,
pageId,
pageTitle,
publishMode,
isTemplate,
templateName,
templateSnapshotUrl,
}: PropsWithChildren<SharePageContainerProps>) => {
useServiceOptional(DesktopStateSynchronizer);
return (
<MainContainer>
{/* share page does not have ViewRoot so the following does not work yet */}
<ViewTitle title={pageTitle || ''} />
<ViewIcon icon="doc" />
<div className={styles.root}>
<AppTabsHeader />
<div className={styles.mainContainer}>
<ShareHeader
pageId={pageId}
publishMode={publishMode}
isTemplate={isTemplate}
templateName={templateName}
snapshotUrl={templateSnapshotUrl}
/>
{children}
</div>
</div>
</MainContainer>
);
};
const SharePageInner = ({ const SharePageInner = ({
workspaceId, workspaceId,
docId, docId,
@@ -274,41 +354,40 @@ const SharePageInner = ({
return; return;
} }
const Container = BUILD_CONFIG.isElectron
? SharePageDesktopContainer
: SharePageWebContainer;
return ( return (
<FrameworkScope scope={workspace.scope}> <FrameworkScope scope={workspace.scope}>
<FrameworkScope scope={page.scope}> <FrameworkScope scope={page.scope}>
<FrameworkScope scope={editor.scope}> <FrameworkScope scope={editor.scope}>
<AppContainer> <AppContainer>
<MainContainer> <Container
<div className={styles.root}> pageTitle={pageTitle}
<div className={styles.mainContainer}> pageId={page.id}
<ShareHeader publishMode={publishMode}
pageId={page.id} isTemplate={isTemplate}
publishMode={publishMode} templateName={templateName}
isTemplate={isTemplate} templateSnapshotUrl={templateSnapshotUrl}
templateName={templateName} >
snapshotUrl={templateSnapshotUrl} <Scrollable.Root>
/> <Scrollable.Viewport
<Scrollable.Root> className={clsx(
<Scrollable.Viewport 'affine-page-viewport',
className={clsx( styles.editorContainer
'affine-page-viewport', )}
styles.editorContainer >
)} <PageDetailEditor onLoad={onEditorLoad} />
> {publishMode === 'page' ? <ShareFooter /> : null}
<PageDetailEditor onLoad={onEditorLoad} /> </Scrollable.Viewport>
{publishMode === 'page' ? <ShareFooter /> : null} <Scrollable.Scrollbar />
</Scrollable.Viewport> </Scrollable.Root>
<Scrollable.Scrollbar /> <EditorOutlineViewer
</Scrollable.Root> editor={editorContainer}
<EditorOutlineViewer show={publishMode === 'page'}
editor={editorContainer} />
show={publishMode === 'page'} </Container>
/>
<SharePageFooter />
</div>
</div>
</MainContainer>
<PeekViewManagerModal /> <PeekViewManagerModal />
</AppContainer> </AppContainer>
</FrameworkScope> </FrameworkScope>

View File

@@ -6,7 +6,6 @@ import { useCallback, useState } from 'react';
import * as styles from './index.css'; import * as styles from './index.css';
// Although it is called an input, it is actually a button.
export function AppDownloadButton({ export function AppDownloadButton({
className, className,
style, style,

View File

@@ -9,6 +9,7 @@ export const navWrapperStyle = style({
zIndex: -1, zIndex: -1,
}, },
}, },
paddingBottom: 8,
selectors: { selectors: {
'&[data-has-border=true]': { '&[data-has-border=true]': {
borderRight: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`, borderRight: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
@@ -16,9 +17,6 @@ export const navWrapperStyle = style({
'&[data-is-floating="true"]': { '&[data-is-floating="true"]': {
backgroundColor: cssVarV2('layer/background/primary'), backgroundColor: cssVarV2('layer/background/primary'),
}, },
'&[data-client-border="true"]': {
paddingBottom: 8,
},
}, },
}); });
export const hoverNavWrapperStyle = style({ export const hoverNavWrapperStyle = style({

View File

@@ -344,6 +344,7 @@ export * from './app-updater-button';
export * from './category-divider'; export * from './category-divider';
export * from './index.css'; export * from './index.css';
export * from './menu-item'; export * from './menu-item';
export * from './open-in-app-card';
export * from './quick-search-input'; export * from './quick-search-input';
export * from './sidebar-containers'; export * from './sidebar-containers';
export * from './sidebar-header'; export * from './sidebar-header';

View File

@@ -0,0 +1 @@
export * from './open-in-app-card';

View File

@@ -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'),
},
},
});

View File

@@ -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>
);
};

View File

@@ -1,3 +1,6 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach } from 'node:test'; import { afterEach } from 'node:test';
import { beforeEach, describe, expect, test, vi } from 'vitest'; import { beforeEach, describe, expect, test, vi } from 'vitest';

View File

@@ -1,3 +1,4 @@
import { channelToScheme } from '@affine/core/utils';
import type { ReferenceParams } from '@blocksuite/affine/blocks'; import type { ReferenceParams } from '@blocksuite/affine/blocks';
import { isNil, pick, pickBy } from 'lodash-es'; import { isNil, pick, pickBy } from 'lodash-es';
import type { ParsedQuery, ParseOptions } from 'query-string'; import type { ParsedQuery, ParseOptions } from 'query-string';
@@ -6,7 +7,6 @@ import queryString from 'query-string';
function maybeAffineOrigin(origin: string, baseUrl: string) { function maybeAffineOrigin(origin: string, baseUrl: string) {
return ( return (
origin.startsWith('file://') || origin.startsWith('file://') ||
origin.startsWith('affine://') ||
origin.endsWith('affine.pro') || // stable/beta origin.endsWith('affine.pro') || // stable/beta
origin.endsWith('affine.fail') || // canary origin.endsWith('affine.fail') || // canary
origin === baseUrl // localhost or self-hosted origin === baseUrl // localhost or self-hosted
@@ -18,6 +18,13 @@ export const resolveRouteLinkMeta = (
baseUrl = location.origin baseUrl = location.origin
) => { ) => {
try { 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); const url = new URL(href, baseUrl);
// check if origin is one of affine's origins // check if origin is one of affine's origins

View File

@@ -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]);
};

View File

@@ -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);
}
}

View File

@@ -1,27 +1,25 @@
import { channelToScheme } from '@affine/core/utils';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { channelToScheme } from './constant';
const logger = new DebugLogger('open-in-app'); const logger = new DebugLogger('open-in-app');
// return an AFFiNE app's url to be opened in desktop app // return an AFFiNE app's url to be opened in desktop app
export const getOpenUrlInDesktopAppLink = ( export const getOpenUrlInDesktopAppLink = (
url: string, url: string,
newTab = false, newTab = true,
scheme = channelToScheme[BUILD_CONFIG.appBuildType] 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 { 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( return new URL(
`${scheme}://${urlObject.host}${urlObject.pathname}?${params.toString()}#${urlObject.hash}` `${scheme}://${urlObject.host}${urlObject.pathname}?${params.toString()}#${urlObject.hash}`
).toString(); ).toString();

View File

@@ -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
);
};

View File

@@ -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,
});

View File

@@ -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>
);
};

View File

@@ -1,3 +1,4 @@
export * from './channel';
export * from './create-emotion-cache'; export * from './create-emotion-cache';
export * from './event'; export * from './event';
export * from './extract-emoji-icon'; export * from './extract-emoji-icon';

View File

@@ -7,16 +7,16 @@
"es-AR": 15, "es-AR": 15,
"es-CL": 17, "es-CL": 17,
"es": 15, "es": 15,
"fr": 75, "fr": 74,
"hi": 2, "hi": 2,
"it": 1, "it": 1,
"ja": 100, "ja": 99,
"ko": 89, "ko": 88,
"pl": 0, "pl": 0,
"pt-BR": 96, "pt-BR": 96,
"ru": 82, "ru": 82,
"sv-SE": 5, "sv-SE": 5,
"ur": 3, "ur": 3,
"zh-Hans": 99, "zh-Hans": 98,
"zh-Hant": 97 "zh-Hant": 96
} }

View File

@@ -229,6 +229,8 @@
"com.affine.auth.open.affine.download-app": "Download app", "com.affine.auth.open.affine.download-app": "Download app",
"com.affine.auth.open.affine.prompt": "Opening <1>AFFiNE</1> app now", "com.affine.auth.open.affine.prompt": "Opening <1>AFFiNE</1> app now",
"com.affine.auth.open.affine.try-again": "Try again", "com.affine.auth.open.affine.try-again": "Try again",
"com.affine.auth.open.affine.doc.open-here": "Open here instead",
"com.affine.auth.open.affine.doc.edit-settings": "Edit settings",
"com.affine.auth.page.sent.email.subtitle": "Please set a password of {{min}}-{{max}} characters with both letters and numbers to continue signing up with ", "com.affine.auth.page.sent.email.subtitle": "Please set a password of {{min}}-{{max}} characters with both letters and numbers to continue signing up with ",
"com.affine.auth.page.sent.email.title": "Welcome to AFFiNE Cloud, you are almost there!", "com.affine.auth.page.sent.email.title": "Welcome to AFFiNE Cloud, you are almost there!",
"com.affine.auth.password": "Password", "com.affine.auth.password": "Password",
@@ -1019,6 +1021,18 @@
"com.affine.settings.appearance.language-description": "Select the language for the interface.", "com.affine.settings.appearance.language-description": "Select the language for the interface.",
"com.affine.settings.appearance.start-week-description": "By default, the week starts on Sunday.", "com.affine.settings.appearance.start-week-description": "By default, the week starts on Sunday.",
"com.affine.settings.appearance.window-frame-description": "Customise appearance of Windows Client.", "com.affine.settings.appearance.window-frame-description": "Customise appearance of Windows Client.",
"com.affine.setting.appearance.links": "Links",
"com.affine.setting.appearance.open-in-app": "Open AFFiNE links",
"com.affine.setting.appearance.open-in-app.hint": "You can choose to open the link in the desktop app or directly in the browser.",
"com.affine.setting.appearance.open-in-app.always-ask": "Ask me each time",
"com.affine.setting.appearance.open-in-app.open-in-desktop-app": "Open links in desktop app",
"com.affine.setting.appearance.open-in-app.open-in-web": "Open links in browser",
"com.affine.setting.appearance.open-in-app.title": "Open AFFiNE links",
"com.affine.open-in-app.card.title": "Open this page in app?",
"com.affine.open-in-app.card.button.open": "Open in app",
"com.affine.open-in-app.card.button.dismiss": "Dismiss",
"com.affine.open-in-app.card.remember": "Remember my choice",
"com.affine.open-in-app.card.download": "Download desktop app",
"com.affine.settings.auto-check-description": "If enabled, it will automatically check for new versions at regular intervals.", "com.affine.settings.auto-check-description": "If enabled, it will automatically check for new versions at regular intervals.",
"com.affine.settings.auto-download-description": "If enabled, new versions will be automatically downloaded to the current device.", "com.affine.settings.auto-download-description": "If enabled, new versions will be automatically downloaded to the current device.",
"com.affine.settings.editorSettings": "Editor", "com.affine.settings.editorSettings": "Editor",