diff --git a/src/commands/CreateReadingLogBackupCommand.ts b/src/commands/CreateReadingLogBackupCommand.ts
new file mode 100644
index 0000000..82776be
--- /dev/null
+++ b/src/commands/CreateReadingLogBackupCommand.ts
@@ -0,0 +1,15 @@
+import type { ReadingLog } from "@utils/ReadingLog";
+import { Command } from "./Command";
+
+export class BackupReadingLogCommand extends Command {
+ constructor(private readonly readingLog: ReadingLog) {
+ super("create-reading-log-backup", "Create Reading Log Backup");
+ }
+
+ async callback() {
+ // @ts-expect-error Moment is provided by Obsidian
+ const timestamp = moment().format("YYYY-MM-DD_HH-mm-ss");
+ const backupFilename = `reading-log-backup_${timestamp}.json`;
+ await this.readingLog.save(backupFilename);
+ }
+}
diff --git a/src/commands/RestoreReadingLogBackupCommand.ts b/src/commands/RestoreReadingLogBackupCommand.ts
new file mode 100644
index 0000000..b1332b1
--- /dev/null
+++ b/src/commands/RestoreReadingLogBackupCommand.ts
@@ -0,0 +1,41 @@
+import type { ReadingLog } from "@utils/ReadingLog";
+import { Command } from "./Command";
+import type { Storage } from "@utils/Storage";
+import { Notice, type App } from "obsidian";
+import { TextSuggestModal } from "@ui/modals/TextSuggestModal";
+
+function basename(path: string) {
+ return path.split("/").pop()!;
+}
+
+export class RestoreReadingLogBackupCommand extends Command {
+ constructor(
+ private readonly app: App,
+ private readonly storage: Storage,
+ private readonly readingLog: ReadingLog
+ ) {
+ super("restore-reading-log-backup", "Restore Reading Log Backup");
+ }
+
+ async callback() {
+ let items = await this.storage.listFiles();
+
+ items = items
+ .map((f) => basename(f))
+ .filter(
+ (f) =>
+ f.startsWith("reading-log-backup_") && f.endsWith(".json")
+ );
+
+ const backupPath = await TextSuggestModal.createAndOpen(
+ this.app,
+ items
+ );
+ if (backupPath) {
+ await this.readingLog.load(backupPath);
+ await this.readingLog.save();
+ } else {
+ new Notice("No backup file selected.");
+ }
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 02c5086..9f5febb 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -15,6 +15,8 @@ import { LogReadingStartedCommand } from "@commands/LogReadingStartedCommand";
import { LogReadingProgressCommand } from "@commands/LogReadingProgressCommand";
import { LogReadingFinishedCommand } from "@commands/LogReadingFinishedCommand";
import { ResetReadingStatusCommand } from "@commands/ResetReadingStatusCommand";
+import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand";
+import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand";
export default class BookTrackerPlugin extends Plugin {
settings: BookTrackerPluginSettings;
@@ -54,6 +56,14 @@ export default class BookTrackerPlugin extends Plugin {
this.settings
)
);
+ this.addCommand(new BackupReadingLogCommand(this.readingLog));
+ this.addCommand(
+ new RestoreReadingLogBackupCommand(
+ this.app,
+ this.storage,
+ this.readingLog
+ )
+ );
this.addSettingTab(new BookTrackerSettingTab(this));
diff --git a/src/ui/components/suggesters/TextInputSuggest.svelte b/src/ui/components/suggesters/TextInputSuggest.svelte
index 430d96f..236770b 100644
--- a/src/ui/components/suggesters/TextInputSuggest.svelte
+++ b/src/ui/components/suggesters/TextInputSuggest.svelte
@@ -122,61 +122,45 @@
await onChange?.(query);
}
- onMount(() => {
- const scope = new Scope();
+ function onkeydown(event: KeyboardEvent) {
+ if (!expanded) return;
- const arrowUpHandler = scope.register(
- [],
- "ArrowUp",
- (event: KeyboardEvent) => {
- if (!event.isComposing) {
- setSelectedItem(selectedIndex - 1, true);
- return false;
- }
- },
- );
-
- const arrowDownHandler = scope.register(
- [],
- "ArrowDown",
- (event: KeyboardEvent) => {
- if (!event.isComposing) {
- setSelectedItem(selectedIndex + 1, true);
- return false;
- }
- },
- );
-
- const enterHandler = scope.register([], "Enter", (ev) => {
- if (!ev.isComposing) {
- selectItem(selectedIndex);
- return false;
- }
- });
-
- const escapeHandler = scope.register([], "Escape", (ev) => {
- if (!ev.isComposing) {
+ switch (event.key) {
+ case "Esc":
+ case "Escape":
+ case "Cancel":
expanded = false;
- return false;
- }
- });
+ break;
+ case "Accept":
+ case "Enter":
+ selectItem(selectedIndex);
+ break;
+ case "ArrowUp":
+ setSelectedItem(selectedIndex - 1, true);
+ break;
+ case "ArrowDown":
+ setSelectedItem(selectedIndex + 1, true);
+ break;
+ case "Tab":
+ setSelectedItem(
+ selectedIndex + (event.shiftKey ? -1 : 1),
+ false,
+ );
+ break;
+ default:
+ return;
+ }
- app.keymap.pushScope(scope);
-
- return () => {
- scope.unregister(arrowUpHandler);
- scope.unregister(arrowDownHandler);
- scope.unregister(enterHandler);
- scope.unregister(escapeHandler);
- app.keymap.popScope(scope);
- };
- });
+ event.preventDefault();
+ event.stopPropagation();
+ }
{#if expanded}
-
-
+
+
{#each items as item, index}
- selectItem(index)}
- onkeydown={(event) =>
- event.key === "Enter" && selectItem(index)}
+ {onkeydown}
onmouseover={() => (selectedIndex = index)}
onfocus={() => (selectedIndex = index)}
role="option"
diff --git a/src/ui/modals/GoodreadsSearchSuggestModal.ts b/src/ui/modals/GoodreadsSearchSuggestModal.ts
index 95d3c1b..760b818 100644
--- a/src/ui/modals/GoodreadsSearchSuggestModal.ts
+++ b/src/ui/modals/GoodreadsSearchSuggestModal.ts
@@ -7,7 +7,7 @@ export class GoodreadsSearchSuggestModal extends SuggestModal {
constructor(
app: App,
private readonly results: SearchResult[],
- private readonly onChoose: (error: any, results: SearchResult[]) => void
+ private readonly onChoose: (results: SearchResult) => void
) {
super(app);
}
@@ -27,7 +27,7 @@ export class GoodreadsSearchSuggestModal extends SuggestModal {
item: SearchResult,
evt: MouseEvent | KeyboardEvent
): void {
- this.onChoose(null, [item]);
+ this.onChoose(item);
}
static async createAndOpen(
@@ -38,13 +38,9 @@ export class GoodreadsSearchSuggestModal extends SuggestModal {
const modal = new GoodreadsSearchSuggestModal(
app,
results,
- (error, results) => {
- if (error) {
- new Notice(`Error: ${error.message}`);
- reject(error);
- } else {
- resolve(results[0]);
- }
+ (results) => {
+ modal.close();
+ resolve(results);
}
);
modal.open();
diff --git a/src/ui/modals/TextSuggestModal.ts b/src/ui/modals/TextSuggestModal.ts
new file mode 100644
index 0000000..3104f7d
--- /dev/null
+++ b/src/ui/modals/TextSuggestModal.ts
@@ -0,0 +1,38 @@
+import { App, SuggestModal } from "obsidian";
+
+export class TextSuggestModal extends SuggestModal {
+ constructor(
+ app: App,
+ private readonly items: string[],
+ private readonly onChoose: (value: string) => void
+ ) {
+ super(app);
+ }
+
+ getSuggestions(query: string): string[] | Promise {
+ return this.items.filter((item) =>
+ item.toLowerCase().includes(query.toLowerCase())
+ );
+ }
+
+ renderSuggestion(value: string, el: HTMLElement): void {
+ el.setText(value);
+ }
+
+ onChooseSuggestion(item: string, evt: MouseEvent | KeyboardEvent): void {
+ this.onChoose(item);
+ }
+
+ static async createAndOpen(
+ app: App,
+ results: string[]
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ const modal = new TextSuggestModal(app, results, (results) => {
+ modal.close();
+ resolve(results);
+ });
+ modal.open();
+ });
+ }
+}
diff --git a/src/utils/ReadingLog.ts b/src/utils/ReadingLog.ts
index 40196f1..6ad094a 100644
--- a/src/utils/ReadingLog.ts
+++ b/src/utils/ReadingLog.ts
@@ -12,14 +12,14 @@ export class ReadingLog {
private entries: ReadingLogEntry[] = [];
public constructor(private readonly storage: Storage) {
- this.loadEntries().catch((error) => {
+ this.load().catch((error) => {
console.error("Failed to load reading log entries:", error);
});
}
- private async loadEntries() {
+ async load(filename = "reading-log.json") {
const entries = await this.storage.readJSON(
- "reading-log.json"
+ filename
);
if (entries) {
this.entries = entries.map((entry) => ({
@@ -35,9 +35,9 @@ export class ReadingLog {
);
}
- private async storeEntries() {
+ async save(filename = "reading-log.json") {
this.sortEntries();
- await this.storage.writeJSON("reading-log.json", this.entries);
+ await this.storage.writeJSON(filename, this.entries);
}
public getEntries(): ReadingLogEntry[] {
@@ -76,12 +76,12 @@ export class ReadingLog {
public async addRawEntry(entry: ReadingLogEntry) {
this.entries.push(entry);
- await this.storeEntries();
+ await this.save();
}
public async removeEntries(book: string): Promise {
this.entries = this.entries.filter((entry) => entry.book !== book);
- await this.storeEntries();
+ await this.save();
}
public async removeLastEntry(book: string): Promise {
@@ -91,23 +91,23 @@ export class ReadingLog {
if (latestEntryIndex !== -1) {
this.entries.splice(latestEntryIndex, 1);
- await this.storeEntries();
+ await this.save();
}
}
public async updateEntry(i: number, entry: ReadingLogEntry): Promise {
this.entries[i] = entry;
- await this.storeEntries();
+ await this.save();
}
public async spliceEntry(i: number): Promise {
const entry = this.entries.splice(i, 1);
- await this.storeEntries();
+ await this.save();
return entry[0];
}
public async clearEntries(): Promise {
this.entries = [];
- await this.storeEntries();
+ await this.save();
}
}
diff --git a/src/utils/Storage.ts b/src/utils/Storage.ts
index 2200ec1..1190b2b 100644
--- a/src/utils/Storage.ts
+++ b/src/utils/Storage.ts
@@ -1,5 +1,5 @@
import BookTrackerPlugin from "@src/main";
-import { App } from "obsidian";
+import { App, normalizePath } from "obsidian";
export class Storage {
public constructor(
@@ -7,8 +7,9 @@ export class Storage {
private readonly plugin: BookTrackerPlugin
) {}
+ private readonly baseDir = this.plugin.manifest.dir!!;
private getFilePath(filename: string): string {
- return `${this.plugin.manifest.dir!!}/${filename}`;
+ return normalizePath(`${this.baseDir}/${filename}`.replace(/\/$/, ""));
}
public async readJSON(filename: string): Promise {
@@ -31,4 +32,16 @@ export class Storage {
const content = JSON.stringify(data, null, 2);
await this.app.vault.adapter.write(filePath, content);
}
+
+ public async listFiles(subdir: string = ""): Promise {
+ const files = await this.app.vault.adapter.list(
+ this.getFilePath(subdir)
+ );
+ return files.files;
+ }
+
+ public async listFolders(): Promise {
+ const files = await this.app.vault.adapter.list(this.baseDir);
+ return files.folders;
+ }
}