mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 13:02:21 +08:00
+3
@@ -30,6 +30,9 @@ export const cardIcon = style({
|
|||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
padding: 4,
|
padding: 4,
|
||||||
lineHeight: 0,
|
lineHeight: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
});
|
});
|
||||||
export const cardContent = style([
|
export const cardContent = style([
|
||||||
spaceY,
|
spaceY,
|
||||||
|
|||||||
+15
-5
@@ -18,7 +18,13 @@ import {
|
|||||||
inputErrorMsg,
|
inputErrorMsg,
|
||||||
} from './index.css';
|
} from './index.css';
|
||||||
|
|
||||||
const ConnectDialog = ({ onClose }: { onClose: () => void }) => {
|
const ConnectDialog = ({
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess: (token: string) => void;
|
||||||
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [status, setStatus] = useState<'idle' | 'verifying' | 'error'>('idle');
|
const [status, setStatus] = useState<'idle' | 'verifying' | 'error'>('idle');
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState('');
|
||||||
@@ -43,7 +49,7 @@ const ConnectDialog = ({ onClose }: { onClose: () => void }) => {
|
|||||||
const handleResult = useCallback(
|
const handleResult = useCallback(
|
||||||
(success: boolean, token: string) => {
|
(success: boolean, token: string) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
readwise.updateSetting('token', token);
|
onSuccess(token);
|
||||||
} else {
|
} else {
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
notify.error({
|
notify.error({
|
||||||
@@ -54,7 +60,7 @@ const ConnectDialog = ({ onClose }: { onClose: () => void }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[readwise, t]
|
[onSuccess, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleConnect = useAsyncCallback(
|
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 t = useI18n();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@@ -143,7 +153,7 @@ export const ConnectButton = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{open && <ConnectDialog onClose={handleClose} />}
|
{open && <ConnectDialog onClose={handleClose} onSuccess={onSuccess} />}
|
||||||
<Button variant="primary" className={actionButton} onClick={handleOpen}>
|
<Button variant="primary" className={actionButton} onClick={handleOpen}>
|
||||||
{t['com.affine.integration.readwise.connect']()}
|
{t['com.affine.integration.readwise.connect']()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+2
-10
@@ -5,7 +5,6 @@ import { useService } from '@toeverything/infra';
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './connected.css';
|
import * as styles from './connected.css';
|
||||||
import { ImportDialog } from './import-dialog';
|
|
||||||
import { actionButton } from './index.css';
|
import { actionButton } from './index.css';
|
||||||
|
|
||||||
export const DisconnectDialog = ({ onClose }: { onClose: () => void }) => {
|
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 t = useI18n();
|
||||||
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);
|
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);
|
||||||
const [showImportDialog, setShowImportDialog] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showDisconnectDialog && (
|
{showDisconnectDialog && (
|
||||||
<DisconnectDialog onClose={() => setShowDisconnectDialog(false)} />
|
<DisconnectDialog onClose={() => setShowDisconnectDialog(false)} />
|
||||||
)}
|
)}
|
||||||
{showImportDialog && (
|
<Button className={actionButton} onClick={onImport}>
|
||||||
<ImportDialog onClose={() => setShowImportDialog(false)} />
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
className={actionButton}
|
|
||||||
onClick={() => setShowImportDialog(true)}
|
|
||||||
>
|
|
||||||
{t['com.affine.integration.readwise.import']()}
|
{t['com.affine.integration.readwise.import']()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
+14
-2
@@ -286,6 +286,12 @@ const HighlightTable = ({
|
|||||||
useState<
|
useState<
|
||||||
Record<ReadwiseHighlight['id'], ReadwiseHighlight['updated_at']>
|
Record<ReadwiseHighlight['id'], ReadwiseHighlight['updated_at']>
|
||||||
>();
|
>();
|
||||||
|
const syncNewHighlights = useLiveData(
|
||||||
|
useMemo(() => readwise.setting$('syncNewHighlights'), [readwise])
|
||||||
|
);
|
||||||
|
const updateStrategy = useLiveData(
|
||||||
|
useMemo(() => readwise.setting$('updateStrategy'), [readwise])
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
readwise
|
readwise
|
||||||
@@ -336,6 +342,12 @@ const HighlightTable = ({
|
|||||||
const highlight = highlights[idx];
|
const highlight = highlights[idx];
|
||||||
const localUpdatedAt = updatedMap?.[highlight.id];
|
const localUpdatedAt = updatedMap?.[highlight.id];
|
||||||
const readwiseUpdatedAt = highlight.updated_at;
|
const readwiseUpdatedAt = highlight.updated_at;
|
||||||
|
const action = readwise.getAction({
|
||||||
|
localUpdatedAt,
|
||||||
|
remoteUpdatedAt: readwiseUpdatedAt,
|
||||||
|
updateStrategy,
|
||||||
|
syncNewHighlights,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<li className={styles.tableBodyRow}>
|
<li className={styles.tableBodyRow}>
|
||||||
<div className={styles.tableCellSelect}>
|
<div className={styles.tableCellSelect}>
|
||||||
@@ -363,11 +375,11 @@ const HighlightTable = ({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.tableCellTodo}>
|
<div className={styles.tableCellTodo}>
|
||||||
{!localUpdatedAt ? (
|
{action === 'new' ? (
|
||||||
<span className={styles.todoNew}>
|
<span className={styles.todoNew}>
|
||||||
{t['com.affine.integration.readwise.import.todo-new']()}
|
{t['com.affine.integration.readwise.import.todo-new']()}
|
||||||
</span>
|
</span>
|
||||||
) : localUpdatedAt === readwiseUpdatedAt ? (
|
) : action === 'skip' ? (
|
||||||
<span className={styles.todoSkip}>
|
<span className={styles.todoSkip}>
|
||||||
{t['com.affine.integration.readwise.import.todo-skip']()}
|
{t['com.affine.integration.readwise.import.todo-skip']()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+41
-2
@@ -4,6 +4,7 @@ import {
|
|||||||
} from '@affine/core/modules/integration';
|
} from '@affine/core/modules/integration';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IntegrationCard,
|
IntegrationCard,
|
||||||
@@ -13,23 +14,61 @@ import {
|
|||||||
} from '../card';
|
} from '../card';
|
||||||
import { ConnectButton } from './connect';
|
import { ConnectButton } from './connect';
|
||||||
import { ConnectedActions } from './connected';
|
import { ConnectedActions } from './connected';
|
||||||
|
import { ImportDialog } from './import-dialog';
|
||||||
|
import { SettingDialog } from './setting-dialog';
|
||||||
|
|
||||||
export const ReadwiseIntegration = () => {
|
export const ReadwiseIntegration = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const readwise = useService(IntegrationService).readwise;
|
const readwise = useService(IntegrationService).readwise;
|
||||||
|
|
||||||
|
const [openSetting, setOpenSetting] = useState(false);
|
||||||
|
const [openImportDialog, setOpenImportDialog] = useState(false);
|
||||||
const settings = useLiveData(readwise.settings$);
|
const settings = useLiveData(readwise.settings$);
|
||||||
const token = settings?.token;
|
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 (
|
return (
|
||||||
<IntegrationCard>
|
<IntegrationCard>
|
||||||
<IntegrationCardHeader icon={<IntegrationTypeIcon type="readwise" />} />
|
<IntegrationCardHeader
|
||||||
|
icon={<IntegrationTypeIcon type="readwise" />}
|
||||||
|
onSettingClick={handleOpenSetting}
|
||||||
|
/>
|
||||||
<IntegrationCardContent
|
<IntegrationCardContent
|
||||||
title={t['com.affine.integration.readwise.name']()}
|
title={t['com.affine.integration.readwise.name']()}
|
||||||
desc={t['com.affine.integration.readwise.desc']()}
|
desc={t['com.affine.integration.readwise.desc']()}
|
||||||
/>
|
/>
|
||||||
<IntegrationCardFooter>
|
<IntegrationCardFooter>
|
||||||
{token ? <ConnectedActions /> : <ConnectButton />}
|
{token ? (
|
||||||
|
<>
|
||||||
|
<ConnectedActions onImport={handleImport} />
|
||||||
|
{openSetting && (
|
||||||
|
<SettingDialog
|
||||||
|
onClose={handleCloseSetting}
|
||||||
|
onImport={handleImport}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{openImportDialog && (
|
||||||
|
<ImportDialog onClose={() => setOpenImportDialog(false)} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ConnectButton onSuccess={handleConnectSuccess} />
|
||||||
|
)}
|
||||||
</IntegrationCardFooter>
|
</IntegrationCardFooter>
|
||||||
</IntegrationCard>
|
</IntegrationCard>
|
||||||
);
|
);
|
||||||
|
|||||||
+81
@@ -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',
|
||||||
|
});
|
||||||
+176
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+62
@@ -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,
|
||||||
|
});
|
||||||
+89
@@ -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);
|
importing$ = new LiveData(false);
|
||||||
settings$ = LiveData.from(this.readwiseStore.watchSetting(), undefined);
|
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>(
|
updateSetting<T extends keyof ReadwiseConfig>(
|
||||||
key: T,
|
key: T,
|
||||||
value: ReadwiseConfig[T]
|
value: ReadwiseConfig[T]
|
||||||
@@ -77,6 +84,8 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
|
|||||||
localRefs.map(ref => [ref.refMeta.highlightId, ref])
|
localRefs.map(ref => [ref.refMeta.highlightId, ref])
|
||||||
);
|
);
|
||||||
const updateStrategy = this.readwiseStore.getSetting('updateStrategy');
|
const updateStrategy = this.readwiseStore.getSetting('updateStrategy');
|
||||||
|
const syncNewHighlights =
|
||||||
|
this.readwiseStore.getSetting('syncNewHighlights');
|
||||||
const chunks = chunk(highlights, 2);
|
const chunks = chunk(highlights, 2);
|
||||||
const total = highlights.length;
|
const total = highlights.length;
|
||||||
let finished = 0;
|
let finished = 0;
|
||||||
@@ -99,8 +108,14 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
|
|||||||
const refMeta = localRef?.refMeta;
|
const refMeta = localRef?.refMeta;
|
||||||
const localUpdatedAt = refMeta?.updatedAt;
|
const localUpdatedAt = refMeta?.updatedAt;
|
||||||
const localDocId = localRef?.id;
|
const localDocId = localRef?.id;
|
||||||
|
const action = this.getAction({
|
||||||
|
localUpdatedAt,
|
||||||
|
remoteUpdatedAt: highlight.updated_at,
|
||||||
|
updateStrategy,
|
||||||
|
syncNewHighlights,
|
||||||
|
});
|
||||||
// write if not matched
|
// write if not matched
|
||||||
if (localUpdatedAt !== highlight.updated_at && !signal?.aborted) {
|
if (action !== 'skip' && !signal?.aborted) {
|
||||||
await this.highlightToAffineDoc(highlight, book, localDocId, {
|
await this.highlightToAffineDoc(highlight, book, localDocId, {
|
||||||
updateStrategy,
|
updateStrategy,
|
||||||
integrationId,
|
integrationId,
|
||||||
@@ -139,7 +154,7 @@ export class ReadwiseIntegration extends Entity<{ writer: IntegrationWriter }> {
|
|||||||
title: book.title,
|
title: book.title,
|
||||||
docId,
|
docId,
|
||||||
comment: highlight.note,
|
comment: highlight.note,
|
||||||
updateStrategy,
|
updateStrategy: updateStrategy ?? 'append',
|
||||||
});
|
});
|
||||||
|
|
||||||
// write failed
|
// 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() {
|
disconnect() {
|
||||||
this.readwiseStore.setSetting('token', undefined);
|
this.readwiseStore.setSettings({
|
||||||
this.readwiseStore.setSetting('lastImportedAt', undefined);
|
token: undefined,
|
||||||
|
updateStrategy: undefined,
|
||||||
|
syncNewHighlights: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,4 +81,11 @@ export class ReadwiseStore extends Store {
|
|||||||
[key]: value,
|
[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
|
* The last import time
|
||||||
*/
|
*/
|
||||||
lastImportedAt?: string;
|
lastImportedAt?: string;
|
||||||
|
/**
|
||||||
|
* Whether to sync new highlights
|
||||||
|
*/
|
||||||
|
syncNewHighlights?: boolean;
|
||||||
/**
|
/**
|
||||||
* The update strategy
|
* The update strategy
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"ar": 95,
|
"ar": 95,
|
||||||
"ca": 4,
|
"ca": 4,
|
||||||
"da": 5,
|
"da": 4,
|
||||||
"de": 95,
|
"de": 95,
|
||||||
"el-GR": 95,
|
"el-GR": 95,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-AR": 95,
|
"es-AR": 95,
|
||||||
"es-CL": 97,
|
"es-CL": 96,
|
||||||
"es": 95,
|
"es": 95,
|
||||||
"fa": 95,
|
"fa": 95,
|
||||||
"fr": 95,
|
"fr": 95,
|
||||||
|
|||||||
@@ -7313,6 +7313,58 @@ export function useAFFiNEI18N(): {
|
|||||||
["com.affine.integration.readwise.import.abort-notify-desc"](options: {
|
["com.affine.integration.readwise.import.abort-notify-desc"](options: {
|
||||||
readonly finished: string;
|
readonly finished: string;
|
||||||
}): 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`
|
* `Author`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1821,6 +1821,19 @@
|
|||||||
"com.affine.integration.readwise.import.importing-stop": "Stop Importing",
|
"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-title": "Importing aborted",
|
||||||
"com.affine.integration.readwise.import.abort-notify-desc": "Import aborted, with {{finished}} highlights processed",
|
"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.author": "Author",
|
||||||
"com.affine.integration.readwise-prop.source": "Source",
|
"com.affine.integration.readwise-prop.source": "Source",
|
||||||
"com.affine.integration.readwise-prop.created": "Created",
|
"com.affine.integration.readwise-prop.created": "Created",
|
||||||
|
|||||||
Reference in New Issue
Block a user