@@ -363,11 +375,11 @@ const HighlightTable = ({
- {!localUpdatedAt ? (
+ {action === 'new' ? (
{t['com.affine.integration.readwise.import.todo-new']()}
- ) : localUpdatedAt === readwiseUpdatedAt ? (
+ ) : action === 'skip' ? (
{t['com.affine.integration.readwise.import.todo-skip']()}
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/index.tsx
index 80a4ec8b0..83936a914 100644
--- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/index.tsx
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/index.tsx
@@ -4,6 +4,7 @@ import {
} from '@affine/core/modules/integration';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
+import { useCallback, useState } from 'react';
import {
IntegrationCard,
@@ -13,23 +14,61 @@ import {
} from '../card';
import { ConnectButton } from './connect';
import { ConnectedActions } from './connected';
+import { ImportDialog } from './import-dialog';
+import { SettingDialog } from './setting-dialog';
export const ReadwiseIntegration = () => {
const t = useI18n();
const readwise = useService(IntegrationService).readwise;
+ const [openSetting, setOpenSetting] = useState(false);
+ const [openImportDialog, setOpenImportDialog] = useState(false);
const settings = useLiveData(readwise.settings$);
const token = settings?.token;
+ const handleOpenSetting = useCallback(() => setOpenSetting(true), []);
+ const handleCloseSetting = useCallback(() => setOpenSetting(false), []);
+
+ const handleConnectSuccess = useCallback(
+ (token: string) => {
+ readwise.connect(token);
+ handleOpenSetting();
+ },
+ [handleOpenSetting, readwise]
+ );
+
+ const handleImport = useCallback(() => {
+ setOpenSetting(false);
+ setOpenImportDialog(true);
+ }, []);
+
return (
- } />
+ }
+ onSettingClick={handleOpenSetting}
+ />
- {token ? : }
+ {token ? (
+ <>
+
+ {openSetting && (
+
+ )}
+ {openImportDialog && (
+ setOpenImportDialog(false)} />
+ )}
+ >
+ ) : (
+
+ )}
);
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/setting-dialog.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/setting-dialog.css.ts
new file mode 100644
index 000000000..f971124c5
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/setting-dialog.css.ts
@@ -0,0 +1,81 @@
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const dialog = style({
+ width: 480,
+ maxWidth: 'calc(100vw - 32px)',
+});
+
+export const header = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+ paddingBottom: 16,
+ borderBottom: '0.5px solid ' + cssVarV2.layer.insideBorder.border,
+});
+export const headerIcon = style({
+ width: 40,
+ height: 40,
+ fontSize: 30,
+ borderRadius: 5,
+});
+export const headerTitle = style({
+ fontSize: 15,
+ lineHeight: '24px',
+ fontWeight: 500,
+ color: cssVarV2.text.primary,
+});
+export const headerCaption = style({
+ fontSize: 12,
+ lineHeight: '20px',
+ fontWeight: 400,
+ color: cssVarV2.text.secondary,
+});
+
+export const settings = style({
+ display: 'flex',
+ flexDirection: 'column',
+ marginTop: 16,
+ gap: 8,
+ alignItems: 'stretch',
+});
+
+export const divider = style({
+ height: 8,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'stretch',
+ selectors: {
+ '&::before': {
+ content: '',
+ width: '100%',
+ height: 0,
+ borderBottom: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
+ flexGrow: 1,
+ },
+ },
+});
+
+export const updateStrategyLabel = style({
+ fontSize: 12,
+ fontWeight: 500,
+ lineHeight: '20px',
+ color: cssVarV2.text.primary,
+ marginBottom: 8,
+});
+export const updateStrategyGroup = style({
+ overflow: 'hidden',
+ display: 'grid',
+ gridTemplateRows: '1fr',
+ transition:
+ 'grid-template-rows 0.4s cubic-bezier(.07,.83,.46,1), opacity 0.4s ease',
+ selectors: {
+ '&[data-collapsed="true"]': {
+ gridTemplateRows: '0fr',
+ opacity: 0,
+ },
+ },
+});
+export const updateStrategyGroupContent = style({
+ overflow: 'hidden',
+});
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/setting-dialog.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/setting-dialog.tsx
new file mode 100644
index 000000000..dd5208031
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/readwise/setting-dialog.tsx
@@ -0,0 +1,176 @@
+import { Button, Modal } from '@affine/component';
+import {
+ IntegrationService,
+ IntegrationTypeIcon,
+} from '@affine/core/modules/integration';
+import type { ReadwiseConfig } from '@affine/core/modules/integration/type';
+import { useI18n } from '@affine/i18n';
+import { useLiveData, useService } from '@toeverything/infra';
+import { useCallback, useMemo } from 'react';
+
+import { IntegrationCardIcon } from '../card';
+import {
+ IntegrationSettingItem,
+ IntegrationSettingTextRadioGroup,
+ type IntegrationSettingTextRadioGroupItem,
+ IntegrationSettingToggle,
+} from '../setting';
+import * as styles from './setting-dialog.css';
+
+export const SettingDialog = ({
+ onClose,
+ onImport,
+}: {
+ onClose: () => void;
+ onImport: () => void;
+}) => {
+ const t = useI18n();
+ return (
+
+
+
+
+ );
+};
+
+const Divider = () => {
+ return
;
+};
+
+const NewHighlightSetting = () => {
+ const t = useI18n();
+ const readwise = useService(IntegrationService).readwise;
+ const syncNewHighlights = useLiveData(
+ useMemo(() => readwise.setting$('syncNewHighlights'), [readwise])
+ );
+
+ const toggle = useCallback(
+ (value: boolean) => {
+ readwise.updateSetting('syncNewHighlights', value);
+ },
+ [readwise]
+ );
+
+ return (
+
+
+
+ );
+};
+
+const UpdateStrategySetting = () => {
+ const t = useI18n();
+ const readwise = useService(IntegrationService).readwise;
+ const updateStrategy = useLiveData(
+ useMemo(() => readwise.setting$('updateStrategy'), [readwise])
+ );
+
+ const toggle = useCallback(
+ (value: boolean) => {
+ if (!value) readwise.updateSetting('updateStrategy', undefined);
+ else readwise.updateSetting('updateStrategy', 'append');
+ },
+ [readwise]
+ );
+
+ const handleUpdate = useCallback(
+ (value: ReadwiseConfig['updateStrategy']) => {
+ readwise.updateSetting('updateStrategy', value);
+ },
+ [readwise]
+ );
+
+ const strategies = useMemo(
+ () =>
+ [
+ {
+ name: t[
+ 'com.affine.integration.readwise.setting.update-append-name'
+ ](),
+ desc: t[
+ 'com.affine.integration.readwise.setting.update-append-desc'
+ ](),
+ value: 'append',
+ },
+ {
+ name: t[
+ 'com.affine.integration.readwise.setting.update-override-name'
+ ](),
+ desc: t[
+ 'com.affine.integration.readwise.setting.update-override-desc'
+ ](),
+ value: 'override',
+ },
+ ] satisfies IntegrationSettingTextRadioGroupItem[],
+ [t]
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+ {t['com.affine.integration.readwise.setting.update-strategy']()}
+
+
+
+
+ >
+ );
+};
+
+const StartImport = ({ onImport }: { onImport: () => void }) => {
+ const t = useI18n();
+ return (
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.css.ts
new file mode 100644
index 000000000..07bcc2d32
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.css.ts
@@ -0,0 +1,62 @@
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const settingItem = style({
+ width: '100%',
+ display: 'flex',
+ alignItems: 'flex-start',
+ justifyContent: 'space-between',
+ gap: 8,
+});
+
+export const settingName = style({
+ fontSize: 14,
+ lineHeight: '22px',
+ fontWeight: 500,
+ color: cssVarV2.text.primary,
+});
+
+export const settingDesc = style({
+ fontSize: 12,
+ lineHeight: '20px',
+ fontWeight: 400,
+ color: cssVarV2.text.secondary,
+});
+
+export const textRadioGroup = style({
+ borderRadius: 4,
+ border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
+});
+export const textRadioGroupItem = style({
+ padding: '8px 16px',
+ display: 'flex',
+ alignItems: 'flex-start',
+ justifyContent: 'space-between',
+ gap: 8,
+ cursor: 'pointer',
+ borderBottom: `1px solid ${cssVarV2.layer.insideBorder.border}`,
+ selectors: {
+ '&:last-child': {
+ borderBottom: 'none',
+ },
+ },
+});
+export const textRadioGroupItemName = style({
+ fontSize: 14,
+ lineHeight: '22px',
+ fontWeight: 500,
+ color: cssVarV2.text.primary,
+});
+export const textRadioGroupItemDesc = style({
+ fontSize: 12,
+ lineHeight: '20px',
+ fontWeight: 400,
+ color: cssVarV2.text.secondary,
+});
+export const textRadioGroupItemCheckWrapper = style({
+ width: 24,
+ height: 24,
+ fontSize: 24,
+ color: cssVarV2.icon.activated,
+ flexShrink: 0,
+});
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.tsx
new file mode 100644
index 000000000..eaf7889ec
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/setting.tsx
@@ -0,0 +1,89 @@
+import { Switch } from '@affine/component';
+import { DoneIcon } from '@blocksuite/icons/rc';
+import clsx from 'clsx';
+import type { HTMLAttributes, ReactNode } from 'react';
+
+import * as styles from './setting.css';
+
+// universal
+export interface IntegrationSettingItemProps
+ extends HTMLAttributes
{
+ name?: ReactNode;
+ desc?: ReactNode;
+}
+export const IntegrationSettingItem = ({
+ name,
+ desc,
+ children,
+ className,
+ ...props
+}: IntegrationSettingItemProps) => {
+ return (
+
+
+ {name &&
{name}
}
+ {desc &&
{desc}
}
+
+
{children}
+
+ );
+};
+
+// toggle
+export interface IntegrationSettingToggleProps {
+ name: string;
+ desc?: string;
+ checked: boolean;
+ onChange: (checked: boolean) => void;
+}
+export const IntegrationSettingToggle = ({
+ name,
+ desc,
+ checked,
+ onChange,
+}: IntegrationSettingToggleProps) => {
+ return (
+
+
+
+ );
+};
+
+// text-radio-group
+export interface IntegrationSettingTextRadioGroupItem {
+ name: string;
+ desc?: string;
+ value: any;
+}
+export interface IntegrationSettingTextRadioGroupProps {
+ items: IntegrationSettingTextRadioGroupItem[];
+ checked: any;
+ onChange: (value: any) => void;
+}
+export const IntegrationSettingTextRadioGroup = ({
+ items,
+ checked,
+ onChange,
+}: IntegrationSettingTextRadioGroupProps) => {
+ return (
+
+ {items.map(item => (
+
onChange(item.value)}
+ key={item.value}
+ className={styles.textRadioGroupItem}
+ >
+
+
{item.name}
+ {item.desc && (
+
{item.desc}
+ )}
+
+
+ {checked === item.value ? : null}
+
+
+ ))}
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/integration/entities/readwise.ts b/packages/frontend/core/src/modules/integration/entities/readwise.ts
index 9bb467e7f..ca262587e 100644
--- a/packages/frontend/core/src/modules/integration/entities/readwise.ts
+++ b/packages/frontend/core/src/modules/integration/entities/readwise.ts
@@ -30,6 +30,13 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
importing$ = new LiveData(false);
settings$ = LiveData.from(this.readwiseStore.watchSetting(), undefined);
+
+ setting$(
+ key: T
+ ): LiveData {
+ return this.settings$.selector(setting => setting?.[key]);
+ }
+
updateSetting(
key: T,
value: ReadwiseConfig[T]
@@ -77,6 +84,8 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
localRefs.map(ref => [ref.refMeta.highlightId, ref])
);
const updateStrategy = this.readwiseStore.getSetting('updateStrategy');
+ const syncNewHighlights =
+ this.readwiseStore.getSetting('syncNewHighlights');
const chunks = chunk(highlights, 2);
const total = highlights.length;
let finished = 0;
@@ -99,8 +108,14 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
const refMeta = localRef?.refMeta;
const localUpdatedAt = refMeta?.updatedAt;
const localDocId = localRef?.id;
+ const action = this.getAction({
+ localUpdatedAt,
+ remoteUpdatedAt: highlight.updated_at,
+ updateStrategy,
+ syncNewHighlights,
+ });
// write if not matched
- if (localUpdatedAt !== highlight.updated_at && !signal?.aborted) {
+ if (action !== 'skip' && !signal?.aborted) {
await this.highlightToAffineDoc(highlight, book, localDocId, {
updateStrategy,
integrationId,
@@ -139,7 +154,7 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
title: book.title,
docId,
comment: highlight.note,
- updateStrategy,
+ updateStrategy: updateStrategy ?? 'append',
});
// write failed
@@ -168,8 +183,43 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
});
}
+ getAction(info: {
+ localUpdatedAt?: string;
+ remoteUpdatedAt?: string;
+ updateStrategy?: ReadwiseConfig['updateStrategy'];
+ syncNewHighlights?: ReadwiseConfig['syncNewHighlights'];
+ }) {
+ const {
+ localUpdatedAt,
+ remoteUpdatedAt,
+ updateStrategy,
+ syncNewHighlights,
+ } = info;
+
+ return !localUpdatedAt
+ ? syncNewHighlights
+ ? 'new'
+ : 'skip'
+ : localUpdatedAt !== remoteUpdatedAt
+ ? updateStrategy
+ ? 'update'
+ : 'skip'
+ : 'skip';
+ }
+
+ connect(token: string) {
+ this.readwiseStore.setSettings({
+ token,
+ updateStrategy: 'append',
+ syncNewHighlights: true,
+ });
+ }
+
disconnect() {
- this.readwiseStore.setSetting('token', undefined);
- this.readwiseStore.setSetting('lastImportedAt', undefined);
+ this.readwiseStore.setSettings({
+ token: undefined,
+ updateStrategy: undefined,
+ syncNewHighlights: undefined,
+ });
}
}
diff --git a/packages/frontend/core/src/modules/integration/store/readwise.ts b/packages/frontend/core/src/modules/integration/store/readwise.ts
index b767da64e..762166e12 100644
--- a/packages/frontend/core/src/modules/integration/store/readwise.ts
+++ b/packages/frontend/core/src/modules/integration/store/readwise.ts
@@ -81,4 +81,11 @@ export class ReadwiseStore extends Store {
[key]: value,
});
}
+
+ setSettings(settings: Partial) {
+ this.globalState.set(this.getStorageKey(), {
+ ...this.getSetting(),
+ ...settings,
+ });
+ }
}
diff --git a/packages/frontend/core/src/modules/integration/type.ts b/packages/frontend/core/src/modules/integration/type.ts
index 795e96105..38e95fdc5 100644
--- a/packages/frontend/core/src/modules/integration/type.ts
+++ b/packages/frontend/core/src/modules/integration/type.ts
@@ -79,6 +79,10 @@ export interface ReadwiseConfig {
* The last import time
*/
lastImportedAt?: string;
+ /**
+ * Whether to sync new highlights
+ */
+ syncNewHighlights?: boolean;
/**
* The update strategy
*/
diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json
index 06b4cc9bc..2683fe530 100644
--- a/packages/frontend/i18n/src/i18n-completenesses.json
+++ b/packages/frontend/i18n/src/i18n-completenesses.json
@@ -1,12 +1,12 @@
{
"ar": 95,
"ca": 4,
- "da": 5,
+ "da": 4,
"de": 95,
"el-GR": 95,
"en": 100,
"es-AR": 95,
- "es-CL": 97,
+ "es-CL": 96,
"es": 95,
"fa": 95,
"fr": 95,
diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts
index 566472649..acdd7153d 100644
--- a/packages/frontend/i18n/src/i18n.gen.ts
+++ b/packages/frontend/i18n/src/i18n.gen.ts
@@ -7313,6 +7313,58 @@ export function useAFFiNEI18N(): {
["com.affine.integration.readwise.import.abort-notify-desc"](options: {
readonly finished: string;
}): string;
+ /**
+ * `Configuration`
+ */
+ ["com.affine.integration.readwise.setting.caption"](): string;
+ /**
+ * `New Readwise highlights will be imported to AFFiNE `
+ */
+ ["com.affine.integration.readwise.setting.sync-new-name"](): string;
+ /**
+ * `New highlights in Readwise will be synced to AFFiNE `
+ */
+ ["com.affine.integration.readwise.setting.sync-new-desc"](): string;
+ /**
+ * `Updates to Readwise highlights will be imported`
+ */
+ ["com.affine.integration.readwise.setting.update-name"](): string;
+ /**
+ * `Enable this, so that we will process updates of existing highlights from Readwise `
+ */
+ ["com.affine.integration.readwise.setting.update-desc"](): string;
+ /**
+ * `How do we handle updates`
+ */
+ ["com.affine.integration.readwise.setting.update-strategy"](): string;
+ /**
+ * `Append new version to the end`
+ */
+ ["com.affine.integration.readwise.setting.update-append-name"](): string;
+ /**
+ * `Cited or modified highlights will have future versions added to the end of them`
+ */
+ ["com.affine.integration.readwise.setting.update-append-desc"](): string;
+ /**
+ * `Overwrite with new version`
+ */
+ ["com.affine.integration.readwise.setting.update-override-name"](): string;
+ /**
+ * `Cited or modified highlights will be overwritten if there are future updates`
+ */
+ ["com.affine.integration.readwise.setting.update-override-desc"](): string;
+ /**
+ * `Start Importing`
+ */
+ ["com.affine.integration.readwise.setting.start-import-name"](): string;
+ /**
+ * `Using the settings above`
+ */
+ ["com.affine.integration.readwise.setting.start-import-desc"](): string;
+ /**
+ * `Import`
+ */
+ ["com.affine.integration.readwise.setting.start-import-button"](): string;
/**
* `Author`
*/
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 11124017e..af11c0188 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -1821,6 +1821,19 @@
"com.affine.integration.readwise.import.importing-stop": "Stop Importing",
"com.affine.integration.readwise.import.abort-notify-title": "Importing aborted",
"com.affine.integration.readwise.import.abort-notify-desc": "Import aborted, with {{finished}} highlights processed",
+ "com.affine.integration.readwise.setting.caption": "Configuration",
+ "com.affine.integration.readwise.setting.sync-new-name": "New Readwise highlights will be imported to AFFiNE ",
+ "com.affine.integration.readwise.setting.sync-new-desc": "New highlights in Readwise will be synced to AFFiNE ",
+ "com.affine.integration.readwise.setting.update-name": "Updates to Readwise highlights will be imported",
+ "com.affine.integration.readwise.setting.update-desc": "Enable this, so that we will process updates of existing highlights from Readwise ",
+ "com.affine.integration.readwise.setting.update-strategy": "How do we handle updates",
+ "com.affine.integration.readwise.setting.update-append-name": "Append new version to the end",
+ "com.affine.integration.readwise.setting.update-append-desc": "Cited or modified highlights will have future versions added to the end of them",
+ "com.affine.integration.readwise.setting.update-override-name": "Overwrite with new version",
+ "com.affine.integration.readwise.setting.update-override-desc": "Cited or modified highlights will be overwritten if there are future updates",
+ "com.affine.integration.readwise.setting.start-import-name": "Start Importing",
+ "com.affine.integration.readwise.setting.start-import-desc": "Using the settings above",
+ "com.affine.integration.readwise.setting.start-import-button": "Import",
"com.affine.integration.readwise-prop.author": "Author",
"com.affine.integration.readwise-prop.source": "Source",
"com.affine.integration.readwise-prop.created": "Created",