mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-02 14:33:54 +03:00
feat(core): add open link in app to doc menu (#8597)
add "open in desktop app" menu item for editor fix AF-1547
This commit is contained in:
parent
5690720652
commit
835fdc33c0
2
packages/common/env/src/constant.ts
vendored
2
packages/common/env/src/constant.ts
vendored
@ -5,7 +5,7 @@ declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __appInfo: {
|
||||
electron: boolean;
|
||||
schema: string;
|
||||
scheme: string;
|
||||
windowName: string;
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
||||
import { CustomThemeModifier } from '@affine/core/modules/theme-editor';
|
||||
import {
|
||||
ClientSchemaProvider,
|
||||
ClientSchemeProvider,
|
||||
PopupWindowProvider,
|
||||
} from '@affine/core/modules/url';
|
||||
import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
||||
@ -71,9 +71,9 @@ framework.impl(PopupWindowProvider, {
|
||||
});
|
||||
},
|
||||
});
|
||||
framework.impl(ClientSchemaProvider, {
|
||||
getClientSchema() {
|
||||
return appInfo?.schema;
|
||||
framework.impl(ClientSchemeProvider, {
|
||||
getClientScheme() {
|
||||
return appInfo?.scheme;
|
||||
},
|
||||
});
|
||||
framework.impl(ValidatorProvider, {
|
||||
|
@ -6,6 +6,7 @@ import { buildType, isDev } from './config';
|
||||
import { logger } from './logger';
|
||||
import { uiSubjects } from './ui';
|
||||
import {
|
||||
addTab,
|
||||
getMainWindow,
|
||||
openUrlInHiddenWindow,
|
||||
openUrlInMainWindow,
|
||||
@ -81,6 +82,24 @@ async function handleAffineUrl(url: string) {
|
||||
method,
|
||||
payload,
|
||||
});
|
||||
} else if (
|
||||
urlObj.searchParams.get('new-tab') &&
|
||||
urlObj.pathname.startsWith('/workspace')
|
||||
) {
|
||||
// @todo(@forehalo): refactor router utilities
|
||||
// basename of /workspace/xxx/yyy is /workspace/xxx
|
||||
const basename = urlObj.pathname.split('/').slice(0, 3).join('/');
|
||||
const pathname = '/' + urlObj.pathname.split('/').slice(3).join('/');
|
||||
|
||||
await addTab({
|
||||
basename,
|
||||
show: true,
|
||||
view: {
|
||||
path: {
|
||||
pathname: pathname,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const hiddenWindow = urlObj.searchParams.get('hidden')
|
||||
? await openUrlInHiddenWindow(urlObj)
|
||||
|
@ -41,9 +41,9 @@ const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']);
|
||||
const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase();
|
||||
const buildType = ReleaseTypeSchema.parse(envBuildType);
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
let schema =
|
||||
let scheme =
|
||||
buildType === 'stable' ? 'affine' : (`affine-${envBuildType}` as Schema);
|
||||
schema = isDev ? 'affine-dev' : schema;
|
||||
scheme = isDev ? 'affine-dev' : scheme;
|
||||
|
||||
export const appInfo = {
|
||||
electron: true,
|
||||
@ -53,7 +53,7 @@ export const appInfo = {
|
||||
viewId:
|
||||
process.argv.find(arg => arg.startsWith('--view-id='))?.split('=')[1] ??
|
||||
'unknown',
|
||||
schema,
|
||||
scheme,
|
||||
};
|
||||
|
||||
function getMainAPIs() {
|
||||
|
@ -7,10 +7,8 @@ import { configureCommonModules } from '@affine/core/modules';
|
||||
import { AuthService, WebSocketAuthProvider } from '@affine/core/modules/cloud';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
|
||||
import {
|
||||
ClientSchemaProvider,
|
||||
PopupWindowProvider,
|
||||
} from '@affine/core/modules/url';
|
||||
import { PopupWindowProvider } from '@affine/core/modules/url';
|
||||
import { ClientSchemeProvider } from '@affine/core/modules/url/providers/client-schema';
|
||||
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
|
||||
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
|
||||
import {
|
||||
@ -51,8 +49,8 @@ framework.impl(PopupWindowProvider, {
|
||||
}).catch(console.error);
|
||||
},
|
||||
});
|
||||
framework.impl(ClientSchemaProvider, {
|
||||
getClientSchema() {
|
||||
framework.impl(ClientSchemeProvider, {
|
||||
getClientScheme() {
|
||||
return 'affine';
|
||||
},
|
||||
});
|
||||
|
@ -35,7 +35,7 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
|
||||
const oauthProviders = useLiveData(
|
||||
serverConfig.config$.map(r => r?.oauthProviders)
|
||||
);
|
||||
const schema = urlService.getClientSchema();
|
||||
const scheme = urlService.getClientScheme();
|
||||
|
||||
if (!oauth) {
|
||||
return <Skeleton height={50} />;
|
||||
@ -46,7 +46,7 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
|
||||
key={provider}
|
||||
provider={provider}
|
||||
redirectUrl={redirectUrl}
|
||||
schema={schema}
|
||||
scheme={scheme}
|
||||
popupWindow={url => {
|
||||
urlService.openPopupWindow(url);
|
||||
}}
|
||||
@ -57,12 +57,12 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
|
||||
function OAuthProvider({
|
||||
provider,
|
||||
redirectUrl,
|
||||
schema,
|
||||
scheme,
|
||||
popupWindow,
|
||||
}: {
|
||||
provider: OAuthProviderType;
|
||||
redirectUrl?: string;
|
||||
schema?: string;
|
||||
scheme?: string;
|
||||
popupWindow: (url: string) => void;
|
||||
}) {
|
||||
const { icon } = OAuthProviderMap[provider];
|
||||
@ -76,8 +76,8 @@ function OAuthProvider({
|
||||
params.set('redirect_uri', redirectUrl);
|
||||
}
|
||||
|
||||
if (schema) {
|
||||
params.set('client', schema);
|
||||
if (scheme) {
|
||||
params.set('client', scheme);
|
||||
}
|
||||
|
||||
// TODO: Android app scheme not implemented
|
||||
@ -87,7 +87,7 @@ function OAuthProvider({
|
||||
BUILD_CONFIG.serverUrlPrefix + `/oauth/login?${params.toString()}`;
|
||||
|
||||
popupWindow(oauthUrl);
|
||||
}, [popupWindow, provider, redirectUrl, schema]);
|
||||
}, [popupWindow, provider, redirectUrl, scheme]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
@ -5,6 +5,10 @@ import {
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
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 { useI18n } from '@affine/i18n';
|
||||
import { mixpanel } from '@affine/track';
|
||||
@ -13,7 +17,6 @@ import { useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
|
||||
import { appIconMap, appNames } from '../../../../../desktop/pages/open-app';
|
||||
import { relatedLinks } from './config';
|
||||
import * as styles from './style.css';
|
||||
import { UpdateCheckSection } from './update-check-section';
|
||||
|
@ -20,8 +20,8 @@ const UpgradeSuccessLayout = ({
|
||||
|
||||
const { jumpToIndex, openInApp } = useNavigateHelper();
|
||||
const openAffine = useCallback(() => {
|
||||
if (params.get('schema')) {
|
||||
openInApp(params.get('schema') ?? 'affine', 'bring-to-front');
|
||||
if (params.get('scheme')) {
|
||||
openInApp(params.get('scheme') ?? 'affine', 'bring-to-front');
|
||||
} else {
|
||||
jumpToIndex();
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
||||
import { DocInfoService } from '@affine/core/modules/doc-info';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app/utils';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
@ -33,6 +34,7 @@ import {
|
||||
HistoryIcon,
|
||||
ImportIcon,
|
||||
InformationIcon,
|
||||
LocalWorkspaceIcon,
|
||||
OpenInNewIcon,
|
||||
PageIcon,
|
||||
SaveIcon,
|
||||
@ -258,6 +260,13 @@ export const PageHeaderMenuButton = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const onOpenInDesktop = useCallback(() => {
|
||||
const url = getOpenUrlInDesktopAppLink(window.location.href, true);
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const EditMenu = (
|
||||
<>
|
||||
{showResponsiveMenu ? ResponsiveMenuItems : null}
|
||||
@ -362,6 +371,15 @@ export const PageHeaderMenuButton = ({
|
||||
data-testid="editor-option-menu-delete"
|
||||
onSelect={handleOpenTrashModal}
|
||||
/>
|
||||
{BUILD_CONFIG.isWeb ? (
|
||||
<MenuItem
|
||||
prefixIcon={<LocalWorkspaceIcon />}
|
||||
data-testid="editor-option-menu-link"
|
||||
onSelect={onOpenInDesktop}
|
||||
>
|
||||
{t['com.affine.header.option.open-in-desktop']()}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
if (isInTrash) {
|
||||
|
@ -160,9 +160,9 @@ export function useNavigateHelper() {
|
||||
);
|
||||
|
||||
const openInApp = useCallback(
|
||||
(schema: string, path: string) => {
|
||||
const encodedUrl = encodeURIComponent(`${schema}://${path}`);
|
||||
return navigate(`/open-app/url?schema=${schema}&url=${encodedUrl}`);
|
||||
(scheme: string, path: string) => {
|
||||
const encodedUrl = encodeURIComponent(`${scheme}://${path}`);
|
||||
return navigate(`/open-app/url?scheme=${scheme}&url=${encodedUrl}`);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
@ -1,52 +1,22 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import {
|
||||
appIconMap,
|
||||
appNames,
|
||||
appSchemes,
|
||||
type Channel,
|
||||
schemeToChannel,
|
||||
} from '@affine/core/modules/open-in-app/constant';
|
||||
import type { GetCurrentUserQuery } from '@affine/graphql';
|
||||
import { fetcher, getCurrentUserQuery } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { Logo1Icon } from '@blocksuite/icons/rc';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { useLoaderData, useSearchParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import * as styles from './open-app.css';
|
||||
|
||||
let lastOpened = '';
|
||||
|
||||
const appSchemas = z.enum([
|
||||
'affine',
|
||||
'affine-canary',
|
||||
'affine-beta',
|
||||
'affine-internal',
|
||||
'affine-dev',
|
||||
]);
|
||||
|
||||
const appChannelSchema = z.enum(['stable', 'canary', 'beta', 'internal']);
|
||||
|
||||
type Schema = z.infer<typeof appSchemas>;
|
||||
type Channel = z.infer<typeof appChannelSchema>;
|
||||
|
||||
const schemaToChanel = {
|
||||
affine: 'stable',
|
||||
'affine-canary': 'canary',
|
||||
'affine-beta': 'beta',
|
||||
'affine-internal': 'internal',
|
||||
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
|
||||
} as Record<Schema, Channel>;
|
||||
|
||||
export const appIconMap = {
|
||||
stable: '/imgs/app-icon-stable.ico',
|
||||
canary: '/imgs/app-icon-canary.ico',
|
||||
beta: '/imgs/app-icon-beta.ico',
|
||||
internal: '/imgs/app-icon-internal.ico',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
export const appNames = {
|
||||
stable: 'AFFiNE',
|
||||
canary: 'AFFiNE Canary',
|
||||
beta: 'AFFiNE Beta',
|
||||
internal: 'AFFiNE Internal',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
interface OpenAppProps {
|
||||
urlToOpen?: string | null;
|
||||
channel: Channel;
|
||||
@ -65,10 +35,8 @@ const OpenAppImpl = ({ urlToOpen, channel }: OpenAppProps) => {
|
||||
}, [channel]);
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
const [params] = useSearchParams();
|
||||
const autoOpen = useMemo(() => params.get('open') !== 'false', [params]);
|
||||
|
||||
if (urlToOpen && lastOpened !== urlToOpen && autoOpen) {
|
||||
if (urlToOpen && lastOpened !== urlToOpen) {
|
||||
lastOpened = urlToOpen;
|
||||
location.href = urlToOpen;
|
||||
}
|
||||
@ -152,9 +120,9 @@ const OpenUrl = () => {
|
||||
params.delete('url');
|
||||
|
||||
const urlObj = new URL(urlToOpen || '');
|
||||
const maybeSchema = appSchemas.safeParse(urlObj.protocol.replace(':', ''));
|
||||
const maybeScheme = appSchemes.safeParse(urlObj.protocol.replace(':', ''));
|
||||
const channel =
|
||||
schemaToChanel[maybeSchema.success ? maybeSchema.data : 'affine'];
|
||||
schemeToChannel[maybeScheme.success ? maybeScheme.data : 'affine'];
|
||||
|
||||
params.forEach((v, k) => {
|
||||
urlObj.searchParams.set(k, v);
|
||||
@ -170,16 +138,16 @@ const OpenOAuthJwt = () => {
|
||||
const { currentUser } = useLoaderData() as LoaderData;
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const maybeSchema = appSchemas.safeParse(params.get('schema'));
|
||||
const schema = maybeSchema.success ? maybeSchema.data : 'affine';
|
||||
const maybeScheme = appSchemes.safeParse(params.get('scheme'));
|
||||
const scheme = maybeScheme.success ? maybeScheme.data : 'affine';
|
||||
const next = params.get('next');
|
||||
const channel = schemaToChanel[schema as Schema];
|
||||
const channel = schemeToChannel[scheme];
|
||||
|
||||
if (!currentUser || !currentUser?.token?.sessionToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlToOpen = `${schema}://signin-redirect?token=${
|
||||
const urlToOpen = `${scheme}://signin-redirect?token=${
|
||||
currentUser.token.sessionToken
|
||||
}&next=${next || ''}`;
|
||||
|
||||
@ -199,12 +167,19 @@ export const Component = () => {
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const action = args.params.action || '';
|
||||
const res = await fetcher({
|
||||
query: getCurrentUserQuery,
|
||||
}).catch(console.error);
|
||||
|
||||
return {
|
||||
action,
|
||||
currentUser: res?.currentUser || null,
|
||||
};
|
||||
if (action === 'signin-redirect') {
|
||||
const res = await fetcher({
|
||||
query: getCurrentUserQuery,
|
||||
}).catch(console.error);
|
||||
|
||||
return {
|
||||
action,
|
||||
currentUser: res?.currentUser || null,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
action,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -119,7 +119,7 @@ export class AuthService extends Service {
|
||||
) {
|
||||
track.$.$.auth.signIn({ method: 'magic-link' });
|
||||
try {
|
||||
const scheme = this.urlService.getClientSchema();
|
||||
const scheme = this.urlService.getClientScheme();
|
||||
const magicLinkUrlParams = new URLSearchParams();
|
||||
if (redirectUrl) {
|
||||
magicLinkUrlParams.set('redirect_uri', redirectUrl);
|
||||
|
@ -23,14 +23,14 @@ const SUBSCRIPTION_CACHE_KEY = 'subscription:';
|
||||
|
||||
const getDefaultSubscriptionSuccessCallbackLink = (
|
||||
plan: SubscriptionPlan | null,
|
||||
schema?: string
|
||||
scheme?: string
|
||||
) => {
|
||||
const path =
|
||||
plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success';
|
||||
const urlString = getAffineCloudBaseUrl() + path;
|
||||
const url = new URL(urlString);
|
||||
if (schema) {
|
||||
url.searchParams.set('schema', schema);
|
||||
if (scheme) {
|
||||
url.searchParams.set('scheme', scheme);
|
||||
}
|
||||
return url.toString();
|
||||
};
|
||||
@ -133,7 +133,7 @@ export class SubscriptionStore extends Store {
|
||||
input.successCallbackLink ||
|
||||
getDefaultSubscriptionSuccessCallbackLink(
|
||||
input.plan,
|
||||
this.urlService.getClientSchema()
|
||||
this.urlService.getClientScheme()
|
||||
),
|
||||
},
|
||||
},
|
||||
|
44
packages/frontend/core/src/modules/open-in-app/constant.ts
Normal file
44
packages/frontend/core/src/modules/open-in-app/constant.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const appSchemes = z.enum([
|
||||
'affine',
|
||||
'affine-canary',
|
||||
'affine-beta',
|
||||
'affine-internal',
|
||||
'affine-dev',
|
||||
]);
|
||||
|
||||
const appChannelSchemes = z.enum(['stable', 'canary', 'beta', 'internal']);
|
||||
|
||||
export type Scheme = z.infer<typeof appSchemes>;
|
||||
export type Channel = z.infer<typeof appChannelSchemes>;
|
||||
|
||||
export const schemeToChannel = {
|
||||
affine: 'stable',
|
||||
'affine-canary': 'canary',
|
||||
'affine-beta': 'beta',
|
||||
'affine-internal': 'internal',
|
||||
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
|
||||
} as Record<Scheme, Channel>;
|
||||
|
||||
export const channelToScheme = {
|
||||
stable: 'affine',
|
||||
canary:
|
||||
process.env.NODE_ENV === 'development' ? 'affine-dev' : 'affine-canary',
|
||||
beta: 'affine-beta',
|
||||
internal: 'affine-internal',
|
||||
} as Record<Channel, Scheme>;
|
||||
|
||||
export const appIconMap = {
|
||||
stable: '/imgs/app-icon-stable.ico',
|
||||
canary: '/imgs/app-icon-canary.ico',
|
||||
beta: '/imgs/app-icon-beta.ico',
|
||||
internal: '/imgs/app-icon-internal.ico',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
export const appNames = {
|
||||
stable: 'AFFiNE',
|
||||
canary: 'AFFiNE Canary',
|
||||
beta: 'AFFiNE Beta',
|
||||
internal: 'AFFiNE Internal',
|
||||
} satisfies Record<Channel, string>;
|
32
packages/frontend/core/src/modules/open-in-app/utils.ts
Normal file
32
packages/frontend/core/src/modules/open-in-app/utils.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
import { channelToScheme } from './constant';
|
||||
|
||||
const logger = new DebugLogger('open-in-app');
|
||||
|
||||
// return an AFFiNE app's url to be opened in desktop app
|
||||
export const getOpenUrlInDesktopAppLink = (
|
||||
url: string,
|
||||
newTab = false,
|
||||
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 {
|
||||
return new URL(
|
||||
`${scheme}://${urlObject.host}${urlObject.pathname}?${params.toString()}#${urlObject.hash}`
|
||||
).toString();
|
||||
} catch (e) {
|
||||
logger.error('Failed to get open url in desktop app link', e);
|
||||
return null;
|
||||
}
|
||||
};
|
@ -1,10 +1,10 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { ClientSchemaProvider } from './providers/client-schema';
|
||||
import { ClientSchemeProvider } from './providers/client-schema';
|
||||
import { PopupWindowProvider } from './providers/popup-window';
|
||||
import { UrlService } from './services/url';
|
||||
|
||||
export { ClientSchemaProvider } from './providers/client-schema';
|
||||
export { ClientSchemeProvider } from './providers/client-schema';
|
||||
export { PopupWindowProvider } from './providers/popup-window';
|
||||
export { UrlService } from './services/url';
|
||||
|
||||
@ -14,7 +14,7 @@ export const configureUrlModule = (container: Framework) => {
|
||||
f =>
|
||||
new UrlService(
|
||||
f.getOptional(PopupWindowProvider),
|
||||
f.getOptional(ClientSchemaProvider)
|
||||
f.getOptional(ClientSchemeProvider)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { createIdentifier } from '@toeverything/infra';
|
||||
|
||||
export interface ClientSchemaProvider {
|
||||
export interface ClientSchemeProvider {
|
||||
/**
|
||||
* Get the client schema in the current environment, used for the user to complete the authentication process in the browser and redirect back to the app.
|
||||
*/
|
||||
getClientSchema(): string | undefined;
|
||||
getClientScheme(): string | undefined;
|
||||
}
|
||||
|
||||
export const ClientSchemaProvider = createIdentifier<ClientSchemaProvider>(
|
||||
'ClientSchemaProvider'
|
||||
export const ClientSchemeProvider = createIdentifier<ClientSchemeProvider>(
|
||||
'ClientSchemeProvider'
|
||||
);
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import type { ClientSchemaProvider } from '../providers/client-schema';
|
||||
import type { ClientSchemeProvider } from '../providers/client-schema';
|
||||
import type { PopupWindowProvider } from '../providers/popup-window';
|
||||
|
||||
export class UrlService extends Service {
|
||||
constructor(
|
||||
// those providers are optional, because they are not always available in some environments
|
||||
private readonly popupWindowProvider?: PopupWindowProvider,
|
||||
private readonly clientSchemaProvider?: ClientSchemaProvider
|
||||
private readonly clientSchemeProvider?: ClientSchemeProvider
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getClientSchema() {
|
||||
return this.clientSchemaProvider?.getClientSchema();
|
||||
getClientScheme() {
|
||||
return this.clientSchemeProvider?.getClientScheme();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { DocCollection } from '@blocksuite/affine/store';
|
||||
@ -27,6 +28,32 @@ export const LOCAL_WORKSPACE_LOCAL_STORAGE_KEY = 'affine-local-workspace';
|
||||
const LOCAL_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY =
|
||||
'affine-local-workspace-changed';
|
||||
|
||||
const logger = new DebugLogger('local-workspace');
|
||||
|
||||
export function getLocalWorkspaceIds(): string[] {
|
||||
try {
|
||||
return JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Failed to get local workspace ids', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function setLocalWorkspaceIds(
|
||||
idsOrUpdater: string[] | ((ids: string[]) => string[])
|
||||
) {
|
||||
localStorage.setItem(
|
||||
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(
|
||||
typeof idsOrUpdater === 'function'
|
||||
? idsOrUpdater(getLocalWorkspaceIds())
|
||||
: idsOrUpdater
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export class LocalWorkspaceFlavourProvider
|
||||
extends Service
|
||||
implements WorkspaceFlavourProvider
|
||||
@ -43,13 +70,7 @@ export class LocalWorkspaceFlavourProvider
|
||||
);
|
||||
|
||||
async deleteWorkspace(id: string): Promise<void> {
|
||||
const allWorkspaceIDs: string[] = JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
);
|
||||
localStorage.setItem(
|
||||
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(allWorkspaceIDs.filter(x => x !== id))
|
||||
);
|
||||
setLocalWorkspaceIds(ids => ids.filter(x => x !== id));
|
||||
|
||||
if (BUILD_CONFIG.isElectron && apis) {
|
||||
await apis.workspace.delete(id);
|
||||
@ -88,14 +109,7 @@ export class LocalWorkspaceFlavourProvider
|
||||
}
|
||||
|
||||
// save workspace id to local storage
|
||||
const allWorkspaceIDs: string[] = JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
);
|
||||
allWorkspaceIDs.push(id);
|
||||
localStorage.setItem(
|
||||
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(allWorkspaceIDs)
|
||||
);
|
||||
setLocalWorkspaceIds(ids => [...ids, id]);
|
||||
|
||||
// notify all browser tabs, so they can update their workspace list
|
||||
this.notifyChannel.postMessage(id);
|
||||
@ -106,9 +120,10 @@ export class LocalWorkspaceFlavourProvider
|
||||
new Observable<WorkspaceMetadata[]>(subscriber => {
|
||||
let last: WorkspaceMetadata[] | null = null;
|
||||
const emit = () => {
|
||||
const value = JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
).map((id: string) => ({ id, flavour: WorkspaceFlavour.LOCAL }));
|
||||
const value = getLocalWorkspaceIds().map(id => ({
|
||||
id,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
}));
|
||||
if (isEqual(last, value)) return;
|
||||
subscriber.next(value);
|
||||
last = value;
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"ar": 85,
|
||||
"ar": 84,
|
||||
"ca": 6,
|
||||
"da": 6,
|
||||
"de": 31,
|
||||
@ -14,7 +14,7 @@
|
||||
"ko": 89,
|
||||
"pl": 0,
|
||||
"pt-BR": 97,
|
||||
"ru": 83,
|
||||
"ru": 82,
|
||||
"sv-SE": 5,
|
||||
"ur": 3,
|
||||
"zh-Hans": 99,
|
||||
|
@ -478,6 +478,7 @@
|
||||
"com.affine.filterList.button.add": "Add filter",
|
||||
"com.affine.header.option.add-tag": "Add tag",
|
||||
"com.affine.header.option.duplicate": "Duplicate",
|
||||
"com.affine.header.option.open-in-desktop": "Open in desktop app",
|
||||
"com.affine.header.option.view-frame": "View all frames",
|
||||
"com.affine.header.option.view-toc": "View table of contents",
|
||||
"com.affine.helpIsland.contactUs": "Contact us",
|
||||
|
Loading…
Reference in New Issue
Block a user