Files
AFFiNE-Mirror/packages/frontend/apps/android/src/app.tsx
2025-03-20 13:47:35 +00:00

316 lines
9.6 KiB
TypeScript

import { AffineContext } from '@affine/core/components/context';
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
import { configureMobileModules } from '@affine/core/mobile/modules';
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
import { router } from '@affine/core/mobile/router';
import { configureCommonModules } from '@affine/core/modules';
import { AIButtonProvider } from '@affine/core/modules/ai-button';
import {
AuthService,
DefaultServerService,
ServersService,
} from '@affine/core/modules/cloud';
import { DocsService } from '@affine/core/modules/doc';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { I18nProvider } from '@affine/core/modules/i18n';
import { LifecycleService } from '@affine/core/modules/lifecycle';
import {
configureLocalStorageStateStorageImpls,
NbstoreProvider,
} from '@affine/core/modules/storage';
import { PopupWindowProvider } from '@affine/core/modules/url';
import { ClientSchemeProvider } from '@affine/core/modules/url/providers/client-schema';
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
import { WorkspacesService } from '@affine/core/modules/workspace';
import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine';
import { I18n } from '@affine/i18n';
import { StoreManagerClient } from '@affine/nbstore/worker/client';
import { defaultBlockMarkdownAdapterMatchers } from '@blocksuite/affine/adapters';
import { Container } from '@blocksuite/affine/global/di';
import {
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine/inlines/preset';
import {
docLinkBaseURLMiddleware,
MarkdownAdapter,
titleMiddleware,
} from '@blocksuite/affine/shared/adapters';
import { App as CapacitorApp } from '@capacitor/app';
import { Keyboard } from '@capacitor/keyboard';
import { StatusBar, Style } from '@capacitor/status-bar';
import { EdgeToEdge } from '@capawesome/capacitor-android-edge-to-edge-support';
import { InAppBrowser } from '@capgo/inappbrowser';
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
import { OpClient } from '@toeverything/infra/op';
import { useTheme } from 'next-themes';
import { Suspense, useEffect } from 'react';
import { RouterProvider } from 'react-router-dom';
import { AffineTheme } from './plugins/affine-theme';
import { AIButton } from './plugins/ai-button';
const storeManagerClient = new StoreManagerClient(
new OpClient(
new Worker(
new URL(/* webpackChunkName: "nbstore" */ './nbstore.ts', import.meta.url)
)
)
);
window.addEventListener('beforeunload', () => {
storeManagerClient.dispose();
});
const future = {
v7_startTransition: true,
} as const;
const framework = new Framework();
configureCommonModules(framework);
configureBrowserWorkbenchModule(framework);
configureLocalStorageStateStorageImpls(framework);
configureBrowserWorkspaceFlavours(framework);
configureMobileModules(framework);
framework.impl(NbstoreProvider, {
openStore(key, options) {
const { store, dispose } = storeManagerClient.open(key, options);
return {
store,
dispose: () => {
dispose();
},
};
},
});
const frameworkProvider = framework.provider();
framework.impl(PopupWindowProvider, {
open: (url: string) => {
InAppBrowser.open({
url: url,
}).catch(console.error);
},
});
framework.impl(ClientSchemeProvider, {
getClientScheme() {
return 'affine';
},
});
framework.impl(VirtualKeyboardProvider, {
show: () => {
Keyboard.show().catch(console.error);
},
hide: () => {
// In some cases, the keyboard will show again. for example, it will show again
// when this function is called in click event of button. It may be a bug of
// android webview or capacitor.
setTimeout(() => {
Keyboard.hide().catch(console.error);
});
},
onChange: callback => {
let disposeRef = {
dispose: () => {},
};
Promise.all([
Keyboard.addListener('keyboardWillShow', info => {
callback({
visible: true,
height: info.keyboardHeight,
});
}),
Keyboard.addListener('keyboardWillHide', () => {
callback({
visible: false,
height: 0,
});
}),
])
.then(handlers => {
disposeRef.dispose = () => {
Promise.all(handlers.map(handler => handler.remove())).catch(
console.error
);
};
})
.catch(console.error);
return () => {
disposeRef.dispose();
};
},
});
framework.impl(AIButtonProvider, {
presentAIButton: () => {
return AIButton.present();
},
dismissAIButton: () => {
return AIButton.dismiss();
},
});
// ------ some apis for native ------
(window as any).getCurrentServerBaseUrl = () => {
const globalContextService = frameworkProvider.get(GlobalContextService);
const currentServerId = globalContextService.globalContext.serverId.get();
const serversService = frameworkProvider.get(ServersService);
const defaultServerService = frameworkProvider.get(DefaultServerService);
const currentServer =
(currentServerId ? serversService.server$(currentServerId).value : null) ??
defaultServerService.server;
return currentServer.baseUrl;
};
(window as any).getCurrentI18nLocale = () => {
return I18n.language;
};
(window as any).getCurrentWorkspaceId = () => {
const globalContextService = frameworkProvider.get(GlobalContextService);
return globalContextService.globalContext.workspaceId.get();
};
(window as any).getCurrentDocId = () => {
const globalContextService = frameworkProvider.get(GlobalContextService);
return globalContextService.globalContext.docId.get();
};
(window as any).getCurrentDocContentInMarkdown = async () => {
const globalContextService = frameworkProvider.get(GlobalContextService);
const currentWorkspaceId =
globalContextService.globalContext.workspaceId.get();
const currentDocId = globalContextService.globalContext.docId.get();
const workspacesService = frameworkProvider.get(WorkspacesService);
const workspaceRef = currentWorkspaceId
? workspacesService.openByWorkspaceId(currentWorkspaceId)
: null;
if (!workspaceRef) {
return;
}
const { workspace, dispose: disposeWorkspace } = workspaceRef;
const docsService = workspace.scope.get(DocsService);
const docRef = currentDocId ? docsService.open(currentDocId) : null;
if (!docRef) {
return;
}
const { doc, release: disposeDoc } = docRef;
try {
const blockSuiteDoc = doc.blockSuiteDoc;
const transformer = blockSuiteDoc.getTransformer([
docLinkBaseURLMiddleware(blockSuiteDoc.workspace.id),
titleMiddleware(blockSuiteDoc.workspace.meta.docMetas),
]);
const snapshot = transformer.docToSnapshot(blockSuiteDoc);
const container = new Container();
[
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
].forEach(ext => {
ext.setup(container);
});
const provider = container.provider();
const adapter = new MarkdownAdapter(transformer, provider);
if (!snapshot) {
return;
}
const markdownResult = await adapter.fromDocSnapshot({
snapshot,
assets: transformer.assetsManager,
});
return markdownResult.file;
} finally {
disposeDoc();
disposeWorkspace();
}
};
// setup application lifecycle events, and emit application start event
window.addEventListener('focus', () => {
frameworkProvider.get(LifecycleService).applicationFocus();
});
frameworkProvider.get(LifecycleService).applicationStart();
CapacitorApp.addListener('appUrlOpen', ({ url }) => {
// try to close browser if it's open
InAppBrowser.close().catch(e => console.error('Failed to close browser', e));
const urlObj = new URL(url);
if (urlObj.hostname === 'authentication') {
const method = urlObj.searchParams.get('method');
const payload = JSON.parse(urlObj.searchParams.get('payload') ?? 'false');
if (
!method ||
(method !== 'magic-link' && method !== 'oauth') ||
!payload
) {
console.error('Invalid authentication url', url);
return;
}
const authService = frameworkProvider
.get(DefaultServerService)
.server.scope.get(AuthService);
if (method === 'oauth') {
authService
.signInOauth(payload.code, payload.state, payload.provider)
.catch(console.error);
} else if (method === 'magic-link') {
authService
.signInMagicLink(payload.email, payload.token)
.catch(console.error);
}
}
}).catch(e => {
console.error(e);
});
const ThemeProvider = () => {
const { resolvedTheme } = useTheme();
useEffect(() => {
StatusBar.setStyle({
style:
resolvedTheme === 'dark'
? Style.Dark
: resolvedTheme === 'light'
? Style.Light
: Style.Default,
}).catch(console.error);
EdgeToEdge.setBackgroundColor({
color: resolvedTheme === 'dark' ? '#000000' : '#F5F5F5',
}).catch(console.error);
AffineTheme.onThemeChanged({
darkMode: resolvedTheme === 'dark',
}).catch(console.error);
}, [resolvedTheme]);
return null;
};
export function App() {
return (
<Suspense>
<FrameworkRoot framework={frameworkProvider}>
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<ThemeProvider />
<RouterProvider
fallbackElement={<AppFallback />}
router={router}
future={future}
/>
</AffineContext>
</I18nProvider>
</FrameworkRoot>
</Suspense>
);
}