generated from tpl/obsidian-sample-plugin
Add reading log backups
This commit is contained in:
parent
3d3e62e1ba
commit
868b7d8cff
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/main.ts
10
src/main.ts
|
@ -15,6 +15,8 @@ import { LogReadingStartedCommand } from "@commands/LogReadingStartedCommand";
|
||||||
import { LogReadingProgressCommand } from "@commands/LogReadingProgressCommand";
|
import { LogReadingProgressCommand } from "@commands/LogReadingProgressCommand";
|
||||||
import { LogReadingFinishedCommand } from "@commands/LogReadingFinishedCommand";
|
import { LogReadingFinishedCommand } from "@commands/LogReadingFinishedCommand";
|
||||||
import { ResetReadingStatusCommand } from "@commands/ResetReadingStatusCommand";
|
import { ResetReadingStatusCommand } from "@commands/ResetReadingStatusCommand";
|
||||||
|
import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand";
|
||||||
|
import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand";
|
||||||
|
|
||||||
export default class BookTrackerPlugin extends Plugin {
|
export default class BookTrackerPlugin extends Plugin {
|
||||||
settings: BookTrackerPluginSettings;
|
settings: BookTrackerPluginSettings;
|
||||||
|
@ -54,6 +56,14 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
this.settings
|
this.settings
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
this.addCommand(new BackupReadingLogCommand(this.readingLog));
|
||||||
|
this.addCommand(
|
||||||
|
new RestoreReadingLogBackupCommand(
|
||||||
|
this.app,
|
||||||
|
this.storage,
|
||||||
|
this.readingLog
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
this.addSettingTab(new BookTrackerSettingTab(this));
|
this.addSettingTab(new BookTrackerSettingTab(this));
|
||||||
|
|
||||||
|
|
|
@ -122,61 +122,45 @@
|
||||||
await onChange?.(query);
|
await onChange?.(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
function onkeydown(event: KeyboardEvent) {
|
||||||
const scope = new Scope();
|
if (!expanded) return;
|
||||||
|
|
||||||
const arrowUpHandler = scope.register(
|
switch (event.key) {
|
||||||
[],
|
case "Esc":
|
||||||
"ArrowUp",
|
case "Escape":
|
||||||
(event: KeyboardEvent) => {
|
case "Cancel":
|
||||||
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) {
|
|
||||||
expanded = false;
|
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);
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
return () => {
|
}
|
||||||
scope.unregister(arrowUpHandler);
|
|
||||||
scope.unregister(arrowDownHandler);
|
|
||||||
scope.unregister(enterHandler);
|
|
||||||
scope.unregister(escapeHandler);
|
|
||||||
app.keymap.popScope(scope);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class:is-loading={loading}
|
class:is-loading={loading}
|
||||||
aria-busy={loading}
|
aria-busy={loading}
|
||||||
use:clickOutside={() => (expanded = false)}
|
use:clickOutside={() => (expanded = false)}
|
||||||
|
onfocusout={() => (expanded = false)}
|
||||||
>
|
>
|
||||||
<div class="search-input-container">
|
<div class="search-input-container">
|
||||||
<input
|
<input
|
||||||
|
@ -203,20 +187,14 @@
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<div class="suggestion-container" use:popperContent>
|
<div id={`${id}-list`} class="suggestion-container" use:popperContent>
|
||||||
<ul
|
<ul bind:this={listEl} role="listbox" class="suggestion">
|
||||||
id={`${id}-list`}
|
|
||||||
bind:this={listEl}
|
|
||||||
role="listbox"
|
|
||||||
class="suggestion"
|
|
||||||
>
|
|
||||||
{#each items as item, index}
|
{#each items as item, index}
|
||||||
<li
|
<li
|
||||||
class="suggestion-item"
|
class="suggestion-item"
|
||||||
class:is-selected={index === selectedIndex}
|
class:is-selected={index === selectedIndex}
|
||||||
onclick={() => selectItem(index)}
|
onclick={() => selectItem(index)}
|
||||||
onkeydown={(event) =>
|
{onkeydown}
|
||||||
event.key === "Enter" && selectItem(index)}
|
|
||||||
onmouseover={() => (selectedIndex = index)}
|
onmouseover={() => (selectedIndex = index)}
|
||||||
onfocus={() => (selectedIndex = index)}
|
onfocus={() => (selectedIndex = index)}
|
||||||
role="option"
|
role="option"
|
||||||
|
|
|
@ -7,7 +7,7 @@ export class GoodreadsSearchSuggestModal extends SuggestModal<SearchResult> {
|
||||||
constructor(
|
constructor(
|
||||||
app: App,
|
app: App,
|
||||||
private readonly results: SearchResult[],
|
private readonly results: SearchResult[],
|
||||||
private readonly onChoose: (error: any, results: SearchResult[]) => void
|
private readonly onChoose: (results: SearchResult) => void
|
||||||
) {
|
) {
|
||||||
super(app);
|
super(app);
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ export class GoodreadsSearchSuggestModal extends SuggestModal<SearchResult> {
|
||||||
item: SearchResult,
|
item: SearchResult,
|
||||||
evt: MouseEvent | KeyboardEvent
|
evt: MouseEvent | KeyboardEvent
|
||||||
): void {
|
): void {
|
||||||
this.onChoose(null, [item]);
|
this.onChoose(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createAndOpen(
|
static async createAndOpen(
|
||||||
|
@ -38,13 +38,9 @@ export class GoodreadsSearchSuggestModal extends SuggestModal<SearchResult> {
|
||||||
const modal = new GoodreadsSearchSuggestModal(
|
const modal = new GoodreadsSearchSuggestModal(
|
||||||
app,
|
app,
|
||||||
results,
|
results,
|
||||||
(error, results) => {
|
(results) => {
|
||||||
if (error) {
|
modal.close();
|
||||||
new Notice(`Error: ${error.message}`);
|
resolve(results);
|
||||||
reject(error);
|
|
||||||
} else {
|
|
||||||
resolve(results[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
modal.open();
|
modal.open();
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { App, SuggestModal } from "obsidian";
|
||||||
|
|
||||||
|
export class TextSuggestModal extends SuggestModal<string> {
|
||||||
|
constructor(
|
||||||
|
app: App,
|
||||||
|
private readonly items: string[],
|
||||||
|
private readonly onChoose: (value: string) => void
|
||||||
|
) {
|
||||||
|
super(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSuggestions(query: string): string[] | Promise<string[]> {
|
||||||
|
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<string | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const modal = new TextSuggestModal(app, results, (results) => {
|
||||||
|
modal.close();
|
||||||
|
resolve(results);
|
||||||
|
});
|
||||||
|
modal.open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,14 +12,14 @@ export class ReadingLog {
|
||||||
private entries: ReadingLogEntry[] = [];
|
private entries: ReadingLogEntry[] = [];
|
||||||
|
|
||||||
public constructor(private readonly storage: Storage) {
|
public constructor(private readonly storage: Storage) {
|
||||||
this.loadEntries().catch((error) => {
|
this.load().catch((error) => {
|
||||||
console.error("Failed to load reading log entries:", 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<ReadingLogEntry[]>(
|
const entries = await this.storage.readJSON<ReadingLogEntry[]>(
|
||||||
"reading-log.json"
|
filename
|
||||||
);
|
);
|
||||||
if (entries) {
|
if (entries) {
|
||||||
this.entries = entries.map((entry) => ({
|
this.entries = entries.map((entry) => ({
|
||||||
|
@ -35,9 +35,9 @@ export class ReadingLog {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async storeEntries() {
|
async save(filename = "reading-log.json") {
|
||||||
this.sortEntries();
|
this.sortEntries();
|
||||||
await this.storage.writeJSON("reading-log.json", this.entries);
|
await this.storage.writeJSON(filename, this.entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEntries(): ReadingLogEntry[] {
|
public getEntries(): ReadingLogEntry[] {
|
||||||
|
@ -76,12 +76,12 @@ export class ReadingLog {
|
||||||
|
|
||||||
public async addRawEntry(entry: ReadingLogEntry) {
|
public async addRawEntry(entry: ReadingLogEntry) {
|
||||||
this.entries.push(entry);
|
this.entries.push(entry);
|
||||||
await this.storeEntries();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeEntries(book: string): Promise<void> {
|
public async removeEntries(book: string): Promise<void> {
|
||||||
this.entries = this.entries.filter((entry) => entry.book !== book);
|
this.entries = this.entries.filter((entry) => entry.book !== book);
|
||||||
await this.storeEntries();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeLastEntry(book: string): Promise<void> {
|
public async removeLastEntry(book: string): Promise<void> {
|
||||||
|
@ -91,23 +91,23 @@ export class ReadingLog {
|
||||||
|
|
||||||
if (latestEntryIndex !== -1) {
|
if (latestEntryIndex !== -1) {
|
||||||
this.entries.splice(latestEntryIndex, 1);
|
this.entries.splice(latestEntryIndex, 1);
|
||||||
await this.storeEntries();
|
await this.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateEntry(i: number, entry: ReadingLogEntry): Promise<void> {
|
public async updateEntry(i: number, entry: ReadingLogEntry): Promise<void> {
|
||||||
this.entries[i] = entry;
|
this.entries[i] = entry;
|
||||||
await this.storeEntries();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async spliceEntry(i: number): Promise<ReadingLogEntry> {
|
public async spliceEntry(i: number): Promise<ReadingLogEntry> {
|
||||||
const entry = this.entries.splice(i, 1);
|
const entry = this.entries.splice(i, 1);
|
||||||
await this.storeEntries();
|
await this.save();
|
||||||
return entry[0];
|
return entry[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async clearEntries(): Promise<void> {
|
public async clearEntries(): Promise<void> {
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
await this.storeEntries();
|
await this.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import BookTrackerPlugin from "@src/main";
|
import BookTrackerPlugin from "@src/main";
|
||||||
import { App } from "obsidian";
|
import { App, normalizePath } from "obsidian";
|
||||||
|
|
||||||
export class Storage {
|
export class Storage {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
@ -7,8 +7,9 @@ export class Storage {
|
||||||
private readonly plugin: BookTrackerPlugin
|
private readonly plugin: BookTrackerPlugin
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private readonly baseDir = this.plugin.manifest.dir!!;
|
||||||
private getFilePath(filename: string): string {
|
private getFilePath(filename: string): string {
|
||||||
return `${this.plugin.manifest.dir!!}/${filename}`;
|
return normalizePath(`${this.baseDir}/${filename}`.replace(/\/$/, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readJSON<T>(filename: string): Promise<T | null> {
|
public async readJSON<T>(filename: string): Promise<T | null> {
|
||||||
|
@ -31,4 +32,16 @@ export class Storage {
|
||||||
const content = JSON.stringify(data, null, 2);
|
const content = JSON.stringify(data, null, 2);
|
||||||
await this.app.vault.adapter.write(filePath, content);
|
await this.app.vault.adapter.write(filePath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async listFiles(subdir: string = ""): Promise<string[]> {
|
||||||
|
const files = await this.app.vault.adapter.list(
|
||||||
|
this.getFilePath(subdir)
|
||||||
|
);
|
||||||
|
return files.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listFolders(): Promise<string[]> {
|
||||||
|
const files = await this.app.vault.adapter.list(this.baseDir);
|
||||||
|
return files.folders;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue