Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor book import logic to be more consistent and user-friendly #43

Merged
merged 1 commit into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions docs/guide/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ For example, below are some valid folder names:
- `imported_notes/apple_books/highlights`
- `3 - Resources/My Books/Apple Books/Unprocessed`

If the highlight folder is not empty and the [Backup highlights](#backup-highlights) setting is enabled, the plugin will save the existing highlights to a backup folder before importing new highlights. If the setting is disabled, the plugin will overwrite the contents of the highlight folder.


## Import highlights on start

- Default value: Turned off
Expand All @@ -24,13 +21,61 @@ Import all highlights from all your books when Obsidian starts. Respects the [Ba
## Backup highlights

- Default value: Turned off
- Backup folder template: `<highlights-folder>-bk-<timestamp>`. For example, `ibooks-highlights-bk-1704060001`.
- Backup template:
- for the highlight folder: `<highlights-folder>-bk-<timestamp>`. For example, `ibooks-highlights-bk-1704060001`.
- for a specific book: `<highlights-file>-bk-<timestamp>`. For example, `Building a Second Brain-bk-1704060001`.

Backup highlights before import.
- When importing all highlights, the [highlight folder](#highlight-folder) contents (see the note below) will be backed up.
- When importing highlights from a specific book, the specific highlights file will be backed up, if it exists.

The backup name is pre-configured based on the template above and cannot be changed.

::: details Examples

**Import all highlights**

Initial state
```plaintext
.
└── ibooks-highlights
├── Atomic Habits - Tiny Changes, Remarkable Results
└── Building a Second Brain
```
After import
```plaintext
.
├── ibooks-highlights
│ └── <newly imported highlights>
└── ibooks-highlights-bk-1723233525489
├── Atomic Habits - Tiny Changes, Remarkable Results
└── Building a Second Brain
```
**Import highlights from a specific book**

Initial state
```plaintext
.
└── ibooks-highlights
├── Atomic Habits - Tiny Changes, Remarkable Results
└── Building a Second Brain
```
After import
```plaintext
.
└── ibooks-highlights
├── Atomic Habits - Tiny Changes, Remarkable Results
├── Atomic Habits - Tiny Changes, Remarkable Results-bk-1723234215251
└── Building a Second Brain
```

:::

Backup highlights folder before import. The backup folder name is pre-configured based on the template above and cannot be changed. The backup is created inside the [highlight folder](#highlight-folder).
> [!NOTE]
> The plugin will back up only the files that are direct children of the [highlight folder](#highlight-folder). If you (for some reason) have a nested folder structure inside the [highlight folder](#highlight-folder), these folders will not be backed up and will be overwritten on import.

> [!WARNING]
> If the setting is disabled, the plugin will overwrite the contents of the [highlight folder](#highlight-folder) on import.
> This behavior will be improved based on the feedback received: [Issue #34](https://github.com/bandantonio/obsidian-apple-books-highlights-plugin/issues/34#issuecomment-2231429171)
> [!TIP]
> To prevent accidental data loss when the setting is turned off, the plugin will display a confirmation dialog before overwriting the existing highlights.

## Highlights sorting criterion

Expand Down
24 changes: 16 additions & 8 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Notice, Plugin } from 'obsidian';
import { IBookHighlightsPluginSearchModal } from './src/search';
import { IBookHighlightsPluginSearchModal, OverwriteBookModal } from './src/search';
import { aggregateBookAndHighlightDetails } from './src/methods/aggregateDetails';
import SaveHighlights from './src/methods/saveHighlightsToVault';
import { AppleBooksHighlightsImportPluginSettings, IBookHighlightsSettingTab } from './src/settings';
Expand All @@ -17,12 +17,18 @@ export default class IBookHighlightsPlugin extends Plugin {
}

this.addRibbonIcon('book-open', this.manifest.name, async () => {
await this.aggregateAndSaveHighlights().then(() => {
new Notice('Apple Books highlights imported successfully');
}).catch((error) => {
new Notice(`[${this.manifest.name}]:\nError importing highlights. Check console for details (⌥ ⌘ I)`, 0);
try {
this.settings.backup
? await this.aggregateAndSaveHighlights().then(() => {
new Notice('Apple Books highlights imported successfully');
}).catch((error) => {
new Notice(`[${this.manifest.name}]:\nError importing highlights. Check console for details (⌥ ⌘ I)`, 0);
console.error(`[${this.manifest.name}]: ${error}`);
})
: new OverwriteBookModal(this.app, this).open();
} catch (error) {
console.error(`[${this.manifest.name}]: ${error}`);
});
}
});

this.addSettingTab(new IBookHighlightsSettingTab(this.app, this));
Expand All @@ -32,7 +38,9 @@ export default class IBookHighlightsPlugin extends Plugin {
name: 'Import all',
callback: async () => {
try {
await this.aggregateAndSaveHighlights();
this.settings.backup
? await this.aggregateAndSaveHighlights()
: new OverwriteBookModal(this.app, this).open();
} catch (error) {
new Notice(`[${this.manifest.name}]:\nError importing highlights. Check console for details (⌥ ⌘ I)`, 0);
console.error(`[${this.manifest.name}]: ${error}`);
Expand Down Expand Up @@ -74,6 +82,6 @@ export default class IBookHighlightsPlugin extends Plugin {
throw ('No highlights found. Make sure you made some highlights in your Apple Books.');
}

await this.saveHighlights.saveHighlightsToVault(highlights);
await this.saveHighlights.saveAllBooksHighlightsToVault(highlights);
}
}
79 changes: 58 additions & 21 deletions src/methods/saveHighlightsToVault.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { App, Vault } from 'obsidian';
import { App, TFile, Vault } from 'obsidian';
import path from 'path';
import { ICombinedBooksAndHighlights } from '../types';
import { AppleBooksHighlightsImportPluginSettings } from '../settings';
import { renderHighlightsTemplate } from './renderHighlightsTemplate';
import { sortHighlights } from 'src/methods/sortHighlights';
import BackupHighlights from 'src/utils/backupHighlights';

export default class SaveHighlights {
private app: App;
Expand All @@ -16,46 +17,82 @@ export default class SaveHighlights {
this.settings = settings;
}

async saveHighlightsToVault(highlights: ICombinedBooksAndHighlights[]): Promise<void> {
const highlightsFolderPath = this.vault.getAbstractFileByPath(
async saveAllBooksHighlightsToVault(highlights: ICombinedBooksAndHighlights[]): Promise<void> {
const highlightsFolderPath = this.vault.getFolderByPath(
this.settings.highlightsFolder
);

const isBackupEnabled = this.settings.backup;

// // Backup highlights folder if backup is enabled
if (highlightsFolderPath) {
if (isBackupEnabled) {
const highlightsFilesToBackup = (await this.vault.adapter.list(highlightsFolderPath.path)).files;
const backupMethods = new BackupHighlights(this.vault, this.settings);
await backupMethods.backupAllHighlights();
} else {
await this.vault.delete(highlightsFolderPath, true);
await this.vault.createFolder(this.settings.highlightsFolder);
}
} else {
await this.vault.createFolder(this.settings.highlightsFolder);
}

const highlightsBackupFolder = `${this.settings.highlightsFolder}-bk-${Date.now()}`;
for (const combinedHighlight of highlights) {
// Order highlights according to the value in settings
const sortedHighlights = sortHighlights(combinedHighlight, this.settings.highlightsSortingCriterion);

await this.vault.createFolder(highlightsBackupFolder);
// Save highlights to vault
const renderedTemplate = await renderHighlightsTemplate(sortedHighlights, this.settings.template);
const filePath = path.join(this.settings.highlightsFolder, `${combinedHighlight.bookTitle}.md`);

highlightsFilesToBackup.forEach(async (file: string) => {
const fileName = path.basename(file);
await this.createNewBookFile(filePath, renderedTemplate);
}
}

await this.vault.adapter.copy(file, path.join(highlightsBackupFolder, fileName))
});
}
async saveSingleBookHighlightsToVault(highlights: ICombinedBooksAndHighlights[], shouldCreateFile: boolean): Promise<void> {
const highlightsFolderPath = this.vault.getFolderByPath(
this.settings.highlightsFolder
);

await this.vault.delete(highlightsFolderPath, true);
if (!highlightsFolderPath) {
await this.vault.createFolder(this.settings.highlightsFolder);
}

await this.vault.createFolder(this.settings.highlightsFolder);

highlights.forEach(async (combinedHighlight: ICombinedBooksAndHighlights) => {
for (const combinedHighlight of highlights) {
// Order highlights according to the value in settings
const sortedHighlights = sortHighlights(combinedHighlight, this.settings.highlightsSortingCriterion);

// Save highlights to vault
const renderedTemplate = await renderHighlightsTemplate(sortedHighlights, this.settings.template);
const filePath = path.join(this.settings.highlightsFolder, `${combinedHighlight.bookTitle}.md`);

await this.vault.create(
filePath,
renderedTemplate
);
});
if (shouldCreateFile) {
await this.createNewBookFile(filePath, renderedTemplate);
} else {
const isBackupEnabled = this.settings.backup;
const backupMethods = new BackupHighlights(this.vault, this.settings);

const vaultFile = this.vault.getFileByPath(filePath) as TFile;

if (isBackupEnabled) {
backupMethods.backupSingleBookHighlights(combinedHighlight.bookTitle);
}

await this.modifyExistingBookFile(vaultFile, renderedTemplate);
}
}
}

async modifyExistingBookFile(file: TFile, data: string): Promise<void> {
await this.vault.modify(
file,
data
);
}

async createNewBookFile(filePath: string, data: string): Promise<void> {
await this.vault.create(
filePath,
data
);
}
}
119 changes: 100 additions & 19 deletions src/search.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
import { App, Notice, SuggestModal } from 'obsidian';
import { App, Modal, Notice, Setting, SuggestModal } from 'obsidian';
import IBookHighlightsPlugin from '../main';
import { ICombinedBooksAndHighlights } from './types';
import { aggregateBookAndHighlightDetails } from './methods/aggregateDetails';
import { checkBookExistence } from './utils/checkBookExistence';

abstract class IBookHighlightsPluginSuggestModal extends SuggestModal<ICombinedBooksAndHighlights> {
plugin: IBookHighlightsPlugin;
constructor(
app: App,
plugin: IBookHighlightsPlugin) {
plugin: IBookHighlightsPlugin
) {
super(app);
this.plugin = plugin;
}
}

export class IBookHighlightsPluginSearchModal extends IBookHighlightsPluginSuggestModal {
async getSuggestions(query: string): Promise<ICombinedBooksAndHighlights[] > {
try {
const allBooks = await aggregateBookAndHighlightDetails();

return allBooks.filter(book => {
const titleMatch = book.bookTitle.toLowerCase().includes(query.toLowerCase());
const authorMatch = book.bookAuthor.toLowerCase().includes(query.toLowerCase());

return titleMatch || authorMatch;
});
} catch (error) {
new Notice(`[${this.plugin.manifest.name}]:\nError importing highlights. Check console for details (⌥ ⌘ I)`, 0);
console.error(`[${this.plugin.manifest.name}]: ${error}`);
return [];
}
async getSuggestions(query: string): Promise<ICombinedBooksAndHighlights[]> {
try {
const allBooks = await aggregateBookAndHighlightDetails();

return allBooks.filter(book => {
const titleMatch = book.bookTitle.toLowerCase().includes(query.toLowerCase());
const authorMatch = book.bookAuthor.toLowerCase().includes(query.toLowerCase());

return titleMatch || authorMatch;
});
}
catch (error) {
new Notice(`[${this.plugin.manifest.name}]:\nError importing highlights. Check console for details (⌥ ⌘ I)`, 0);
console.error(`[${this.plugin.manifest.name}]: ${error}`);
return [];
}
}

renderSuggestion(value: ICombinedBooksAndHighlights, el: HTMLElement) {
Expand All @@ -36,7 +40,84 @@ export class IBookHighlightsPluginSearchModal extends IBookHighlightsPluginSugge
}

//eslint-disable-next-line
onChooseSuggestion(item: ICombinedBooksAndHighlights, event: MouseEvent | KeyboardEvent) {
this.plugin.saveHighlights.saveHighlightsToVault([item]);
async onChooseSuggestion(item: ICombinedBooksAndHighlights, event: MouseEvent | KeyboardEvent) {
const doesBookFileExist = checkBookExistence(item.bookTitle, this.app.vault, this.plugin.settings);

const isBackupEnabled = this.plugin.settings.backup;

if (!doesBookFileExist && !isBackupEnabled ||
!doesBookFileExist && isBackupEnabled
) {
this.plugin.saveHighlights.saveSingleBookHighlightsToVault([item], true);

} else if (doesBookFileExist && !isBackupEnabled) {
new OverwriteBookModal(this.app, this.plugin, item).open();

} else if (doesBookFileExist && isBackupEnabled) {
this.plugin.saveHighlights.saveSingleBookHighlightsToVault([item], false);
} else {
this.plugin.saveHighlights.saveSingleBookHighlightsToVault([item], true);
}
}
}

// This class is used to display a modal that asks for the user's consent
// to overwrite the existing book in the highlights folder
// It takes an optional `item` parameter with the selected book highlights
// When the parameter is not provided, the modal asks for the consent
// to overwrite all the books
export class OverwriteBookModal extends Modal {
plugin: IBookHighlightsPlugin;
item?: ICombinedBooksAndHighlights;

constructor(
app: App,
plugin: IBookHighlightsPlugin,
item?: ICombinedBooksAndHighlights
) {
super(app);
this.plugin = plugin;
this.item = item;
}

onOpen() {
const { contentEl } = this;
const bookToOverwrite = this.item;

if (bookToOverwrite) {
contentEl.createEl('p', { text: `The selected book already exists in your highlights folder:` });
contentEl.createEl('p', { text: `${bookToOverwrite.bookTitle}`, cls: 'modal-rewrite-book-title'});
contentEl.createEl('p', { text: 'Would you like to proceed with the overwrite?' });
} else {
contentEl.createEl('span', { text: `Bulk import will overwrite` });
contentEl.createEl('span', { text: ` ALL THE BOOKS `, cls: 'modal-rewrite-all-books' });
contentEl.createEl('span', { text: `in your highlights folder` });
contentEl.createEl('p', { text: 'Would you like to proceed with the overwrite?' });
}

new Setting(contentEl)
.addButton(YesButton => {
YesButton.setButtonText('Yes')
.setCta()
.onClick(() => {
bookToOverwrite
? this.plugin.saveHighlights.saveSingleBookHighlightsToVault([bookToOverwrite], false)
: this.plugin.aggregateAndSaveHighlights();

this.close();
});
})

.addButton(NoButton => {
NoButton.setButtonText('No')
.onClick(() => {
this.close();
});
});
}

onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
8 changes: 7 additions & 1 deletion src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ export class IBookHighlightsSettingTab extends PluginSettingTab {

new Setting(containerEl)
.setName('Backup highlights')
.setDesc('Backup highlights folder before import. Backup folder template: <highlights-folder>-bk-<timestamp> (For example, ibooks-highlights-bk-1704060001)')
.setDesc(createFragment(el => {
el.appendText('Backup highlights before import.')
el.createEl('br')
el.appendText('- Folder template: <highlights-folder>-bk-<timestamp> (For example, ibooks-highlights-bk-1704060001).')
el.createEl('br')
el.appendText('- File template: <highlights-file>-bk-<timestamp> (For example, Building a Second Brain-bk-1704060001).')
}))
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.backup)
.onChange(async (value) => {
Expand Down
Loading
Loading