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,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) {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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']()}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './open-in-app-card';
|
||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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 { afterEach } from 'node:test';
|
||||||
|
|
||||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
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 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
|
||||||
|
|||||||
14
packages/frontend/core/src/modules/open-in-app/index.ts
Normal file
14
packages/frontend/core/src/modules/open-in-app/index.ts
Normal 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]);
|
||||||
|
};
|
||||||
100
packages/frontend/core/src/modules/open-in-app/services/index.ts
Normal file
100
packages/frontend/core/src/modules/open-in-app/services/index.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user