mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
@@ -30,6 +30,9 @@ export const cardIcon = style({
|
||||
fontSize: 24,
|
||||
padding: 4,
|
||||
lineHeight: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
export const cardContent = style([
|
||||
spaceY,
|
||||
|
||||
@@ -18,7 +18,13 @@ import {
|
||||
inputErrorMsg,
|
||||
} from './index.css';
|
||||
|
||||
const ConnectDialog = ({ onClose }: { onClose: () => void }) => {
|
||||
const ConnectDialog = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: (token: string) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [status, setStatus] = useState<'idle' | 'verifying' | 'error'>('idle');
|
||||
const [token, setToken] = useState('');
|
||||
@@ -43,7 +49,7 @@ const ConnectDialog = ({ onClose }: { onClose: () => void }) => {
|
||||
const handleResult = useCallback(
|
||||
(success: boolean, token: string) => {
|
||||
if (success) {
|
||||
readwise.updateSetting('token', token);
|
||||
onSuccess(token);
|
||||
} else {
|
||||
setStatus('error');
|
||||
notify.error({
|
||||
@@ -54,7 +60,7 @@ const ConnectDialog = ({ onClose }: { onClose: () => void }) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[readwise, t]
|
||||
[onSuccess, t]
|
||||
);
|
||||
|
||||
const handleConnect = useAsyncCallback(
|
||||
@@ -129,7 +135,11 @@ const ConnectDialog = ({ onClose }: { onClose: () => void }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectButton = () => {
|
||||
export const ConnectButton = ({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess: (token: string) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -143,7 +153,7 @@ export const ConnectButton = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{open && <ConnectDialog onClose={handleClose} />}
|
||||
{open && <ConnectDialog onClose={handleClose} onSuccess={onSuccess} />}
|
||||
<Button variant="primary" className={actionButton} onClick={handleOpen}>
|
||||
{t['com.affine.integration.readwise.connect']()}
|
||||
</Button>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import * as styles from './connected.css';
|
||||
import { ImportDialog } from './import-dialog';
|
||||
import { actionButton } from './index.css';
|
||||
|
||||
export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => {
|
||||
@@ -54,23 +53,16 @@ export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectedActions = () => {
|
||||
export const ConnectedActions = ({ onImport }: { onImport: () => void }) => {
|
||||
const t = useI18n();
|
||||
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);
|
||||
const [showImportDialog, setShowImportDialog] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showDisconnectDialog && (
|
||||
<DisconnectDialog onClose={() => setShowDisconnectDialog(false)} />
|
||||
)}
|
||||
{showImportDialog && (
|
||||
<ImportDialog onClose={() => setShowImportDialog(false)} />
|
||||
)}
|
||||
<Button
|
||||
className={actionButton}
|
||||
onClick={() => setShowImportDialog(true)}
|
||||
>
|
||||
<Button className={actionButton} onClick={onImport}>
|
||||
{t['com.affine.integration.readwise.import']()}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -286,6 +286,12 @@ const HighlightTable = ({
|
||||
useState<
|
||||
Record<ReadwiseHighlight['id'], ReadwiseHighlight['updated_at']>
|
||||
>();
|
||||
const syncNewHighlights = useLiveData(
|
||||
useMemo(() => readwise.setting$('syncNewHighlights'), [readwise])
|
||||
);
|
||||
const updateStrategy = useLiveData(
|
||||
useMemo(() => readwise.setting$('updateStrategy'), [readwise])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
readwise
|
||||
@@ -336,6 +342,12 @@ const HighlightTable = ({
|
||||
const highlight = highlights[idx];
|
||||
const localUpdatedAt = updatedMap?.[highlight.id];
|
||||
const readwiseUpdatedAt = highlight.updated_at;
|
||||
const action = readwise.getAction({
|
||||
localUpdatedAt,
|
||||
remoteUpdatedAt: readwiseUpdatedAt,
|
||||
updateStrategy,
|
||||
syncNewHighlights,
|
||||
});
|
||||
return (
|
||||
<li className={styles.tableBodyRow}>
|
||||
<div className={styles.tableCellSelect}>
|
||||
@@ -363,11 +375,11 @@ const HighlightTable = ({
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.tableCellTodo}>
|
||||
{!localUpdatedAt ? (
|
||||
{action === 'new' ? (
|
||||
<span className={styles.todoNew}>
|
||||
{t['com.affine.integration.readwise.import.todo-new']()}
|
||||
</span>
|
||||
) : localUpdatedAt === readwiseUpdatedAt ? (
|
||||
) : action === 'skip' ? (
|
||||
<span className={styles.todoSkip}>
|
||||
{t['com.affine.integration.readwise.import.todo-skip']()}
|
||||
</span>
|
||||
|
||||
@@ -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 (
|
||||
<IntegrationCard>
|
||||
<IntegrationCardHeader icon={<IntegrationTypeIcon type="readwise" />} />
|
||||
<IntegrationCardHeader
|
||||
icon={<IntegrationTypeIcon type="readwise" />}
|
||||
onSettingClick={handleOpenSetting}
|
||||
/>
|
||||
<IntegrationCardContent
|
||||
title={t['com.affine.integration.readwise.name']()}
|
||||
desc={t['com.affine.integration.readwise.desc']()}
|
||||
/>
|
||||
<IntegrationCardFooter>
|
||||
{token ? <ConnectedActions /> : <ConnectButton />}
|
||||
{token ? (
|
||||
<>
|
||||
<ConnectedActions onImport={handleImport} />
|
||||
{openSetting && (
|
||||
<SettingDialog
|
||||
onClose={handleCloseSetting}
|
||||
onImport={handleImport}
|
||||
/>
|
||||
)}
|
||||
{openImportDialog && (
|
||||
<ImportDialog onClose={() => setOpenImportDialog(false)} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ConnectButton onSuccess={handleConnectSuccess} />
|
||||
)}
|
||||
</IntegrationCardFooter>
|
||||
</IntegrationCard>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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 (
|
||||
<Modal
|
||||
open
|
||||
onOpenChange={onClose}
|
||||
contentOptions={{ className: styles.dialog }}
|
||||
>
|
||||
<header className={styles.header}>
|
||||
<IntegrationCardIcon className={styles.headerIcon}>
|
||||
<IntegrationTypeIcon type="readwise" />
|
||||
</IntegrationCardIcon>
|
||||
<div>
|
||||
<h1 className={styles.headerTitle}>
|
||||
{t['com.affine.integration.readwise.name']()}
|
||||
</h1>
|
||||
<p className={styles.headerCaption}>
|
||||
{t['com.affine.integration.readwise.setting.caption']()}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<ul className={styles.settings}>
|
||||
<NewHighlightSetting />
|
||||
<Divider />
|
||||
<UpdateStrategySetting />
|
||||
<Divider />
|
||||
<StartImport onImport={onImport} />
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const Divider = () => {
|
||||
return <li className={styles.divider} />;
|
||||
};
|
||||
|
||||
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 (
|
||||
<li>
|
||||
<IntegrationSettingToggle
|
||||
checked={!!syncNewHighlights}
|
||||
name={t['com.affine.integration.readwise.setting.sync-new-name']()}
|
||||
desc={t['com.affine.integration.readwise.setting.sync-new-desc']()}
|
||||
onChange={toggle}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<li>
|
||||
<IntegrationSettingToggle
|
||||
checked={!!updateStrategy}
|
||||
name={t['com.affine.integration.readwise.setting.update-name']()}
|
||||
desc={t['com.affine.integration.readwise.setting.update-desc']()}
|
||||
onChange={toggle}
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
className={styles.updateStrategyGroup}
|
||||
data-collapsed={!updateStrategy}
|
||||
>
|
||||
<div className={styles.updateStrategyGroupContent}>
|
||||
<h6 className={styles.updateStrategyLabel}>
|
||||
{t['com.affine.integration.readwise.setting.update-strategy']()}
|
||||
</h6>
|
||||
<IntegrationSettingTextRadioGroup
|
||||
items={strategies}
|
||||
checked={updateStrategy}
|
||||
onChange={handleUpdate}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const StartImport = ({ onImport }: { onImport: () => void }) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<IntegrationSettingItem
|
||||
name={t['com.affine.integration.readwise.setting.start-import-name']()}
|
||||
desc={t['com.affine.integration.readwise.setting.start-import-desc']()}
|
||||
>
|
||||
<Button onClick={onImport}>
|
||||
{t['com.affine.integration.readwise.setting.start-import-button']()}
|
||||
</Button>
|
||||
</IntegrationSettingItem>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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<HTMLDivElement> {
|
||||
name?: ReactNode;
|
||||
desc?: ReactNode;
|
||||
}
|
||||
export const IntegrationSettingItem = ({
|
||||
name,
|
||||
desc,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: IntegrationSettingItemProps) => {
|
||||
return (
|
||||
<div className={clsx(styles.settingItem, className)} {...props}>
|
||||
<div>
|
||||
{name && <h6 className={styles.settingName}>{name}</h6>}
|
||||
{desc && <p className={styles.settingDesc}>{desc}</p>}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// toggle
|
||||
export interface IntegrationSettingToggleProps {
|
||||
name: string;
|
||||
desc?: string;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
export const IntegrationSettingToggle = ({
|
||||
name,
|
||||
desc,
|
||||
checked,
|
||||
onChange,
|
||||
}: IntegrationSettingToggleProps) => {
|
||||
return (
|
||||
<IntegrationSettingItem name={name} desc={desc}>
|
||||
<Switch checked={checked} onChange={onChange} />
|
||||
</IntegrationSettingItem>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className={styles.textRadioGroup}>
|
||||
{items.map(item => (
|
||||
<div
|
||||
onClick={() => onChange(item.value)}
|
||||
key={item.value}
|
||||
className={styles.textRadioGroupItem}
|
||||
>
|
||||
<div>
|
||||
<div className={styles.textRadioGroupItemName}>{item.name}</div>
|
||||
{item.desc && (
|
||||
<div className={styles.textRadioGroupItemDesc}>{item.desc}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.textRadioGroupItemCheckWrapper}>
|
||||
{checked === item.value ? <DoneIcon /> : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -30,6 +30,13 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
|
||||
|
||||
importing$ = new LiveData(false);
|
||||
settings$ = LiveData.from(this.readwiseStore.watchSetting(), undefined);
|
||||
|
||||
setting$<T extends keyof ReadwiseConfig>(
|
||||
key: T
|
||||
): LiveData<ReadwiseConfig[T]> {
|
||||
return this.settings$.selector(setting => setting?.[key]);
|
||||
}
|
||||
|
||||
updateSetting<T extends keyof ReadwiseConfig>(
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,4 +81,11 @@ export class ReadwiseStore extends Store {
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
|
||||
setSettings(settings: Partial<ReadwiseConfig>) {
|
||||
this.globalState.set(this.getStorageKey(), {
|
||||
...this.getSetting(),
|
||||
...settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ export interface ReadwiseConfig {
|
||||
* The last import time
|
||||
*/
|
||||
lastImportedAt?: string;
|
||||
/**
|
||||
* Whether to sync new highlights
|
||||
*/
|
||||
syncNewHighlights?: boolean;
|
||||
/**
|
||||
* The update strategy
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user