generated from tpl/obsidian-sample-plugin
Convert settings to svelte
This commit is contained in:
parent
42ba4c306f
commit
ad0af3d369
|
@ -175,7 +175,7 @@ export function createBookFromNextData(
|
|||
publishedAt: new Date(bookData.details.publicationTime),
|
||||
genres: bookData.bookGenres.map((genre) => genre.genre.name),
|
||||
coverImageUrl: bookData.imageUrl,
|
||||
pageLength: bookData.details.numPages,
|
||||
pageCount: bookData.details.numPages,
|
||||
isbn: bookData.details.isbn,
|
||||
isbn13: bookData.details.isbn13,
|
||||
};
|
||||
|
|
38
src/main.ts
38
src/main.ts
|
@ -64,7 +64,7 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
callback: () => this.resetReadingStatus(),
|
||||
});
|
||||
|
||||
this.addSettingTab(new BookTrackerSettingTab(this.app, this));
|
||||
this.addSettingTab(new BookTrackerSettingTab(this));
|
||||
|
||||
registerReadingLogCodeBlockProcessor(this);
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
throw new Error("Unsupported content type: " + contentType);
|
||||
}
|
||||
|
||||
let filePath = this.settings.coverDirectory + "/";
|
||||
let filePath = this.settings.coverFolder + "/";
|
||||
if (this.settings.groupCoversByFirstLetter) {
|
||||
let groupName = fileName.charAt(0).toUpperCase();
|
||||
if (!/^[A-Z]$/.test(groupName)) {
|
||||
|
@ -150,7 +150,7 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
|
||||
if (renderedContent) {
|
||||
await this.app.vault.create(
|
||||
this.settings.tbrDirectory + "/" + fileName + ".md",
|
||||
this.settings.tbrFolder + "/" + fileName + ".md",
|
||||
renderedContent
|
||||
);
|
||||
}
|
||||
|
@ -228,17 +228,17 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
}
|
||||
|
||||
const fileName = activeFile.basename;
|
||||
if (!this.settings.pageLengthProperty) {
|
||||
new Notice("Page length property is not set in settings.");
|
||||
if (!this.settings.pageCountProperty) {
|
||||
new Notice("Page count property is not set in settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
const pageLength =
|
||||
const pageCount =
|
||||
(this.app.metadataCache.getFileCache(activeFile)?.frontmatter?.[
|
||||
this.settings.pageLengthProperty
|
||||
this.settings.pageCountProperty
|
||||
] as number | undefined) ?? 0;
|
||||
|
||||
if (pageLength <= 0) {
|
||||
if (pageCount <= 0) {
|
||||
new Notice(
|
||||
"Page length property is not set or is invalid in the active file."
|
||||
);
|
||||
|
@ -247,19 +247,19 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
|
||||
const pageNumber = await ReadingProgressModal.createAndOpen(
|
||||
this.app,
|
||||
pageLength
|
||||
pageCount
|
||||
);
|
||||
|
||||
if (pageNumber <= 0 || pageNumber > pageLength) {
|
||||
if (pageNumber <= 0 || pageNumber > pageCount) {
|
||||
new Notice(
|
||||
`Invalid page number: ${pageNumber}. It must be between 1 and ${pageLength}.`
|
||||
`Invalid page number: ${pageNumber}. It must be between 1 and ${pageCount}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.readingLog.addEntry(fileName, pageNumber, pageLength);
|
||||
await this.readingLog.addEntry(fileName, pageNumber, pageCount);
|
||||
new Notice(
|
||||
`Logged reading progress for ${fileName}: Page ${pageNumber} of ${pageLength}.`
|
||||
`Logged reading progress for ${fileName}: Page ${pageNumber} of ${pageCount}.`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -290,14 +290,14 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
const pageLength =
|
||||
const pageCount =
|
||||
(this.app.metadataCache.getFileCache(activeFile)?.frontmatter?.[
|
||||
this.settings.pageLengthProperty
|
||||
this.settings.pageCountProperty
|
||||
] as number | undefined) ?? 0;
|
||||
|
||||
if (pageLength <= 0) {
|
||||
if (pageCount <= 0) {
|
||||
new Notice(
|
||||
"Page length property is not set or is invalid in the active file."
|
||||
"Page count property is not set or is invalid in the active file."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -306,8 +306,8 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
|
||||
await this.readingLog.addEntry(
|
||||
activeFile.basename,
|
||||
pageLength,
|
||||
pageLength
|
||||
pageCount,
|
||||
pageCount
|
||||
);
|
||||
|
||||
// @ts-expect-error Moment is provided by Obsidian
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface Book {
|
|||
publishedAt: Date;
|
||||
genres: string[];
|
||||
coverImageUrl: string;
|
||||
pageLength: number;
|
||||
pageCount: number;
|
||||
isbn: string;
|
||||
isbn13: string;
|
||||
}
|
||||
|
|
|
@ -38,10 +38,6 @@
|
|||
}));
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
console.log(items);
|
||||
});
|
||||
|
||||
function calcSliderPos(e: MouseEvent) {
|
||||
return (e.offsetX / (e.target as HTMLElement).clientWidth) * maxV;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import Item from "./Item.svelte";
|
||||
import type { App } from "obsidian";
|
||||
import FieldSuggest from "../suggesters/FieldSuggest.svelte";
|
||||
|
||||
type Props = Omit<ComponentProps<typeof Item>, "control"> & {
|
||||
app: App;
|
||||
id: string;
|
||||
value?: string;
|
||||
accepts?: string[];
|
||||
};
|
||||
|
||||
let {
|
||||
name,
|
||||
description,
|
||||
app,
|
||||
id,
|
||||
value = $bindable(),
|
||||
accepts,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<Item {name} {description}>
|
||||
{#snippet control()}
|
||||
<FieldSuggest {id} {app} asString bind:value {accepts} />
|
||||
{/snippet}
|
||||
</Item>
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import Item from "./Item.svelte";
|
||||
import type { App, TFile } from "obsidian";
|
||||
import FileSuggest from "../suggesters/FileSuggest.svelte";
|
||||
|
||||
type Props = Omit<ComponentProps<typeof Item>, "control"> & {
|
||||
app: App;
|
||||
id: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
let { name, description, app, id, value = $bindable() }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Item {name} {description}>
|
||||
{#snippet control()}
|
||||
<FileSuggest {id} {app} asString bind:value />
|
||||
{/snippet}
|
||||
</Item>
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import Item from "./Item.svelte";
|
||||
import type { App, TFolder } from "obsidian";
|
||||
import FolderSuggest from "../suggesters/FolderSuggest.svelte";
|
||||
|
||||
type Props = Omit<ComponentProps<typeof Item>, "control"> & {
|
||||
app: App;
|
||||
id: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
let { name, description, app, id, value = $bindable() }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Item {name} {description}>
|
||||
{#snippet control()}
|
||||
<FolderSuggest {id} {app} asString bind:value />
|
||||
{/snippet}
|
||||
</Item>
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
type Props = {
|
||||
title: string | Snippet;
|
||||
description?: string | Snippet;
|
||||
};
|
||||
|
||||
let { title, description }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="setting-item setting-item-heading">
|
||||
<p class="title">
|
||||
{#if typeof title === "string"}
|
||||
{title}
|
||||
{:else}
|
||||
{@render title()}
|
||||
{/if}
|
||||
</p>
|
||||
{#if description}
|
||||
<p class="description">
|
||||
{#if typeof description === "string"}
|
||||
{description}
|
||||
{:else}
|
||||
{@render description()}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
type Props = {
|
||||
name: Snippet | string;
|
||||
description?: Snippet | string;
|
||||
control: Snippet;
|
||||
};
|
||||
|
||||
let { name, description, control }: Props = $props();
|
||||
|
||||
const descriptionLines = $derived(
|
||||
typeof description === "string" ? description.split("\n") : [],
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">
|
||||
{#if typeof name === "string"}
|
||||
{name}
|
||||
{:else}
|
||||
{@render name()}
|
||||
{/if}
|
||||
</div>
|
||||
{#if description}
|
||||
<div class="setting-item-description">
|
||||
{#if typeof description === "string"}
|
||||
{#each descriptionLines as line}
|
||||
<div>{line}</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{@render description()}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="setting-item-control">{@render control()}</div>
|
||||
</div>
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import Item from "./Item.svelte";
|
||||
|
||||
type Props = Omit<ComponentProps<typeof Item>, "control"> & {
|
||||
id?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
let { name, description, id, value = $bindable() }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Item {name} {description}>
|
||||
{#snippet control()}
|
||||
<input {id} type="text" bind:value />
|
||||
{/snippet}
|
||||
</Item>
|
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import Item from "./Item.svelte";
|
||||
|
||||
type Props = Omit<ComponentProps<typeof Item>, "control"> & {
|
||||
id?: string;
|
||||
checked?: boolean;
|
||||
};
|
||||
|
||||
let { name, description, id, checked = $bindable() }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Item {name} {description}>
|
||||
{#snippet control()}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- input only covers part of the toggle element. onclick here covers the rest -->
|
||||
<div
|
||||
class="checkbox-container"
|
||||
class:is-enabled={checked}
|
||||
onclick={() => (checked = !checked)}
|
||||
>
|
||||
<input {id} type="checkbox" bind:checked tabindex="0" />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Item>
|
|
@ -0,0 +1,60 @@
|
|||
<script module lang="ts">
|
||||
export type Field = {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { App } from "obsidian";
|
||||
import TextInputSuggest, { type Item } from "./TextInputSuggest.svelte";
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
id: string;
|
||||
asString?: boolean;
|
||||
value?: Field | string;
|
||||
accepts?: string[];
|
||||
onSelected?: (fieldOrName: Field | string) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
app,
|
||||
id,
|
||||
asString,
|
||||
value = $bindable(),
|
||||
accepts,
|
||||
onSelected,
|
||||
}: Props = $props();
|
||||
|
||||
let items: Item<Field | string>[] = $state([]);
|
||||
|
||||
async function handleChange(query: string) {
|
||||
const typesContent = await this.app.vault.adapter.read(
|
||||
this.app.vault.configDir + "/types.json",
|
||||
);
|
||||
const types = JSON.parse(typesContent).types as Record<string, string>;
|
||||
|
||||
items = Object.entries(types)
|
||||
.filter(([name, type]) => {
|
||||
if (accepts && !accepts.includes(type as string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return name.toLowerCase().includes(query.toLowerCase());
|
||||
})
|
||||
.map(([name, type]) => ({
|
||||
text: name,
|
||||
value: asString ? name : { name, type },
|
||||
}));
|
||||
}
|
||||
</script>
|
||||
|
||||
<TextInputSuggest
|
||||
{app}
|
||||
{id}
|
||||
{items}
|
||||
bind:value
|
||||
onChange={handleChange}
|
||||
{onSelected}
|
||||
/>
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import type { App, TFile } from "obsidian";
|
||||
import TextInputSuggest, { type Item } from "./TextInputSuggest.svelte";
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
id: string;
|
||||
asString?: boolean;
|
||||
value?: TFile | string;
|
||||
onSelected?: (fileOrPath: TFile | string) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
app,
|
||||
id,
|
||||
asString,
|
||||
value = $bindable(),
|
||||
onSelected,
|
||||
}: Props = $props();
|
||||
|
||||
let items: Item<TFile | string>[] = $state([]);
|
||||
|
||||
function handleChange(query: string) {
|
||||
items = app.vault
|
||||
.getMarkdownFiles()
|
||||
.filter((f) => f.path.toLowerCase().includes(query.toLowerCase()))
|
||||
.map((f) => ({ text: f.path, value: asString ? f.path : f }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<TextInputSuggest
|
||||
{app}
|
||||
{id}
|
||||
{items}
|
||||
bind:value
|
||||
onChange={handleChange}
|
||||
{onSelected}
|
||||
/>
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import { TFolder, type App } from "obsidian";
|
||||
import TextInputSuggest, { type Item } from "./TextInputSuggest.svelte";
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
id: string;
|
||||
asString?: boolean;
|
||||
value?: TFolder | string;
|
||||
onSelected?: (folderOrPath: TFolder | string) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
app,
|
||||
id,
|
||||
asString,
|
||||
value = $bindable(),
|
||||
onSelected,
|
||||
}: Props = $props();
|
||||
|
||||
let items: Item<TFolder | string>[] = $state([]);
|
||||
|
||||
function handleChange(query: string) {
|
||||
items = app.vault
|
||||
.getAllFolders()
|
||||
.filter((f) => f.path.toLowerCase().includes(query.toLowerCase()))
|
||||
.map((f) => ({ text: f.path, value: asString ? f.path : f }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<TextInputSuggest
|
||||
{app}
|
||||
{id}
|
||||
{items}
|
||||
bind:value
|
||||
onChange={handleChange}
|
||||
{onSelected}
|
||||
/>
|
|
@ -0,0 +1,235 @@
|
|||
<script lang="ts" module>
|
||||
export type Item<T> = {
|
||||
text: string;
|
||||
value: T;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { clickOutside } from "@ui/directives";
|
||||
|
||||
import { App, Scope } from "obsidian";
|
||||
import { onMount, type Snippet } from "svelte";
|
||||
import { createPopperActions } from "svelte-popperjs";
|
||||
|
||||
type T = $$Generic;
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
id: string;
|
||||
items: Item<T>[];
|
||||
value?: T;
|
||||
loading?: boolean;
|
||||
suggestion?: Snippet<[Item<T>]>;
|
||||
onChange?: (text: string) => void | Promise<void>;
|
||||
onSelected?: (value: T) => void | Promise<void>;
|
||||
};
|
||||
|
||||
let {
|
||||
app,
|
||||
id,
|
||||
items,
|
||||
value = $bindable(),
|
||||
loading = false,
|
||||
suggestion,
|
||||
onChange,
|
||||
onSelected,
|
||||
}: Props = $props();
|
||||
|
||||
let query = $state("");
|
||||
let expanded = $state(false);
|
||||
let selectedIndex = $state(0);
|
||||
let listEl: HTMLUListElement | null = $state(null);
|
||||
|
||||
onMount(async () => {
|
||||
await onChange?.(query);
|
||||
});
|
||||
|
||||
$inspect(value, query, items);
|
||||
$effect.root(() => {
|
||||
function findIndex(value: T | undefined) {
|
||||
return items.findIndex((item) => item.value === value);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const idx = findIndex(value);
|
||||
if (idx !== -1) {
|
||||
const item = items[idx];
|
||||
query = item.text;
|
||||
selectedIndex = idx;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const [popperRef, popperContent] = createPopperActions({
|
||||
placement: "bottom-start",
|
||||
strategy: "fixed",
|
||||
modifiers: [
|
||||
{
|
||||
name: "sameWidth",
|
||||
enabled: true,
|
||||
fn: ({ state, instance }) => {
|
||||
// Note: positioning needs to be calculated twice -
|
||||
// first pass - positioning it according to the width of the popper
|
||||
// second pass - position it with the width bound to the reference element
|
||||
// we need to early exit to avoid an infinite loop
|
||||
const targetWidth = `${state.rects.reference.width}px`;
|
||||
if (state.styles.popper.width === targetWidth) {
|
||||
return;
|
||||
}
|
||||
state.styles.popper.width = targetWidth;
|
||||
instance.update();
|
||||
},
|
||||
phase: "beforeWrite",
|
||||
requires: ["computeStyles"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function wrapAround(value: number, size: number): number {
|
||||
return ((value % size) + size) % size;
|
||||
}
|
||||
|
||||
function setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
|
||||
selectedIndex = wrapAround(selectedIndex, items.length);
|
||||
|
||||
if (scrollIntoView && listEl) {
|
||||
listEl.children[selectedIndex].scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function selectItem(index: number) {
|
||||
const item = items[index];
|
||||
if (!item) return;
|
||||
const { text: newQuery, value: newValue } = item;
|
||||
|
||||
selectedIndex = index;
|
||||
expanded = false;
|
||||
query = newQuery;
|
||||
value = newValue;
|
||||
|
||||
await onSelected?.(newValue);
|
||||
}
|
||||
|
||||
async function handleInput() {
|
||||
expanded = true;
|
||||
await onChange?.(query);
|
||||
}
|
||||
|
||||
async function clearSearch() {
|
||||
query = "";
|
||||
value = undefined;
|
||||
await onChange?.(query);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const scope = new Scope();
|
||||
|
||||
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) {
|
||||
expanded = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
app.keymap.pushScope(scope);
|
||||
|
||||
return () => {
|
||||
scope.unregister(arrowUpHandler);
|
||||
scope.unregister(arrowDownHandler);
|
||||
scope.unregister(enterHandler);
|
||||
scope.unregister(escapeHandler);
|
||||
app.keymap.popScope(scope);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:is-loading={loading}
|
||||
aria-busy={loading}
|
||||
use:clickOutside={() => (expanded = false)}
|
||||
>
|
||||
<div class="search-input-container">
|
||||
<input
|
||||
{id}
|
||||
use:popperRef
|
||||
type="search"
|
||||
enterkeyhint="search"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
role="combobox"
|
||||
bind:value={query}
|
||||
oninput={handleInput}
|
||||
onfocusin={() => (expanded = true)}
|
||||
aria-controls={`${id}-list`}
|
||||
aria-autocomplete="list"
|
||||
aria-expanded={expanded}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="search-input-clear-button"
|
||||
aria-label="Clear search"
|
||||
onclick={clearSearch}
|
||||
onkeydown={(e) => e.key === "Enter" && clearSearch()}
|
||||
></div>
|
||||
</div>
|
||||
{#if expanded}
|
||||
<div class="suggestion-container" use:popperContent>
|
||||
<ul
|
||||
id={`${id}-list`}
|
||||
bind:this={listEl}
|
||||
role="listbox"
|
||||
class="suggestion"
|
||||
>
|
||||
{#each items as item, index}
|
||||
<li
|
||||
class="suggestion-item"
|
||||
class:is-selected={index === selectedIndex}
|
||||
onclick={() => selectItem(index)}
|
||||
onkeydown={(event) =>
|
||||
event.key === "Enter" && selectItem(index)}
|
||||
onmouseover={() => (selectedIndex = index)}
|
||||
onfocus={() => (selectedIndex = index)}
|
||||
role="option"
|
||||
aria-selected={index === selectedIndex}
|
||||
>
|
||||
{#if suggestion}
|
||||
{@render suggestion(item)}
|
||||
{:else}
|
||||
{item.text}
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
export function clickOutside(node: Node, cb: () => void) {
|
||||
function handleClick(event: MouseEvent) {
|
||||
if (
|
||||
node &&
|
||||
!node.contains(event.target as Node) &&
|
||||
!event.defaultPrevented
|
||||
) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
document.addEventListener("click", handleClick, true);
|
||||
return {
|
||||
update() {},
|
||||
destroy() {
|
||||
document.removeEventListener("click", handleClick, true);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { clickOutside } from "./clickOutside";
|
|
@ -7,19 +7,19 @@ export class ReadingProgressModal extends SvelteModal<
|
|||
> {
|
||||
constructor(
|
||||
app: App,
|
||||
pageLength: number,
|
||||
pageCount: number,
|
||||
onSubmit: (pageNumber: number) => void = () => {}
|
||||
) {
|
||||
super(app, ReadingProgressModalView, {
|
||||
props: { pageLength, onSubmit },
|
||||
props: { pageCount, onSubmit },
|
||||
});
|
||||
}
|
||||
|
||||
static createAndOpen(app: App, pageLength: number): Promise<number> {
|
||||
static createAndOpen(app: App, pageCount: number): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new ReadingProgressModal(
|
||||
app,
|
||||
pageLength,
|
||||
pageCount,
|
||||
(pageNumber: number) => {
|
||||
modal.close();
|
||||
resolve(pageNumber);
|
||||
|
|
|
@ -1,112 +1,136 @@
|
|||
<script lang="ts">
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
interface Props {
|
||||
pageLength: number;
|
||||
onSubmit: (pageNumber: number) => void;
|
||||
}
|
||||
let { pageLength, onSubmit }: Props = $props();
|
||||
interface Props {
|
||||
pageCount: number;
|
||||
onSubmit: (pageNumber: number) => void;
|
||||
}
|
||||
let { pageCount, onSubmit }: Props = $props();
|
||||
|
||||
let value = $state(0);
|
||||
let mode: "page-number" | "percentage" = $state("page-number");
|
||||
let value = $state(0);
|
||||
let mode: "page-number" | "percentage" = $state("page-number");
|
||||
|
||||
const label = $derived(mode === "page-number" ? "Page Number" : "Percentage");
|
||||
const placeholder = $derived(
|
||||
mode === "page-number" ? "Enter page number" : "Enter percentage (0-100)"
|
||||
);
|
||||
const min = $derived(0);
|
||||
const max = $derived(
|
||||
mode === "page-number" ? pageLength : 100
|
||||
);
|
||||
const label = $derived(
|
||||
mode === "page-number" ? "Page Number" : "Percentage",
|
||||
);
|
||||
const placeholder = $derived(
|
||||
mode === "page-number"
|
||||
? "Enter page number"
|
||||
: "Enter percentage (0-100)",
|
||||
);
|
||||
const min = $derived(0);
|
||||
const max = $derived(mode === "page-number" ? pageCount : 100);
|
||||
|
||||
function onsubmit(ev: SubmitEvent) {
|
||||
ev.preventDefault();
|
||||
if (value < min || value > max) {
|
||||
new Notice(`Value must be between ${min} and ${max}.`);
|
||||
return;
|
||||
}
|
||||
function onsubmit(ev: SubmitEvent) {
|
||||
ev.preventDefault();
|
||||
if (value < min || value > max) {
|
||||
new Notice(`Value must be between ${min} and ${max}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(mode === "page-number" ? value : Math.round((value / 100) * pageLength));
|
||||
}
|
||||
onSubmit(
|
||||
mode === "page-number"
|
||||
? value
|
||||
: Math.round((value / 100) * pageCount),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="obt-reading-progress">
|
||||
<h2>Log Reading Progress</h2>
|
||||
<form {onsubmit}>
|
||||
<div class="value-field">
|
||||
<label for="value">{label}</label>
|
||||
<input id="value" type="number" {placeholder} {min} {max} bind:value />
|
||||
</div>
|
||||
<div class="mode-field">
|
||||
<label class="mode-field-option page-number">
|
||||
<span>Page Number</span>
|
||||
<input name="mode" type="radio" value="page-number" bind:group={mode} />
|
||||
</label>
|
||||
<label class="mode-field-option percentage">
|
||||
<span>Percentage</span>
|
||||
<input name="mode" type="radio" value="percentage" bind:group={mode} />
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<h2>Log Reading Progress</h2>
|
||||
<form {onsubmit}>
|
||||
<div class="value-field">
|
||||
<label for="value">{label}</label>
|
||||
<input
|
||||
id="value"
|
||||
type="number"
|
||||
{placeholder}
|
||||
{min}
|
||||
{max}
|
||||
bind:value
|
||||
/>
|
||||
</div>
|
||||
<div class="mode-field">
|
||||
<label class="mode-field-option page-number">
|
||||
<span>Page Number</span>
|
||||
<input
|
||||
name="mode"
|
||||
type="radio"
|
||||
value="page-number"
|
||||
bind:group={mode}
|
||||
/>
|
||||
</label>
|
||||
<label class="mode-field-option percentage">
|
||||
<span>Percentage</span>
|
||||
<input
|
||||
name="mode"
|
||||
type="radio"
|
||||
value="percentage"
|
||||
bind:group={mode}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.obt-reading-progress {
|
||||
padding-bottom: var(--size-4-4);
|
||||
.obt-reading-progress {
|
||||
padding-bottom: var(--size-4-4);
|
||||
|
||||
h2 {
|
||||
margin-bottom: var(--size-4-6);
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--size-4-6);
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-4);
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-4);
|
||||
|
||||
.value-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--size-4-2);
|
||||
width: 100%;
|
||||
}
|
||||
.value-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--size-4-2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mode-field {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
.mode-field {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.mode-field-option {
|
||||
text-align: center;
|
||||
padding: var(--size-4-2);
|
||||
background-color: var(--interactive-normal);
|
||||
border: var(--border-width) solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
.mode-field-option {
|
||||
text-align: center;
|
||||
padding: var(--size-4-2);
|
||||
background-color: var(--interactive-normal);
|
||||
border: var(--border-width) solid
|
||||
var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
|
||||
&:has(input:checked) {
|
||||
background-color: var(--interactive-accent);
|
||||
}
|
||||
&:has(input:checked) {
|
||||
background-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.page-number {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
&.page-number {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&.percentage {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
&.percentage {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,303 +1,30 @@
|
|||
import BookTrackerPlugin from "@src/main";
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import { FileSuggest, FolderSuggest, FieldSuggest } from "@ui/suggesters";
|
||||
import { App, PluginSettingTab } from "obsidian";
|
||||
import BookTrackerSettingTabView from "./BookTrackerSettingTabView.svelte";
|
||||
import { mount, unmount } from "svelte";
|
||||
|
||||
export class BookTrackerSettingTab extends PluginSettingTab {
|
||||
constructor(app: App, private plugin: BookTrackerPlugin) {
|
||||
super(app, plugin);
|
||||
}
|
||||
private component: ReturnType<typeof BookTrackerSettingTabView> | undefined;
|
||||
|
||||
heading(text: string): void {
|
||||
const header = document.createDocumentFragment();
|
||||
header.createEl("h2", { text });
|
||||
new Setting(this.containerEl).setHeading().setName(text);
|
||||
constructor(private readonly plugin: BookTrackerPlugin) {
|
||||
super(plugin.app, plugin);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
this.containerEl.empty();
|
||||
this.containerEl.classList.add("obt-settings");
|
||||
|
||||
this.heading("Book Creation Settings");
|
||||
this.templateFileSetting();
|
||||
this.tbrDirectorySetting();
|
||||
this.fileNameFormatSetting();
|
||||
|
||||
this.heading("Cover Download Settings");
|
||||
this.downloadCoversSetting();
|
||||
this.coverDirectorySetting();
|
||||
this.groupCoversByFirstLetterSetting();
|
||||
this.overwriteExistingCoversSetting();
|
||||
|
||||
this.heading("Reading Progress Settings");
|
||||
this.statusPropertySetting();
|
||||
this.startDatePropertySetting();
|
||||
this.endDatePropertySetting();
|
||||
this.ratingPropertySetting();
|
||||
this.pageLengthPropertySetting();
|
||||
this.readingLogDirectorySetting();
|
||||
}
|
||||
|
||||
readingLogDirectorySetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Reading Log Directory")
|
||||
.setDesc("Select the directory where reading logs will be stored")
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
|
||||
cb.setPlaceholder("reading-logs")
|
||||
.setValue(this.plugin.settings.readingLogDirectory)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.readingLogDirectory = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pageLengthPropertySetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Page Length Property")
|
||||
.setDesc(
|
||||
"Property used to track the total number of pages in a book."
|
||||
)
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FieldSuggest(this.app, cb.inputEl, ["number"]);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
|
||||
cb.setPlaceholder("pageLength")
|
||||
.setValue(this.plugin.settings.pageLengthProperty)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.pageLengthProperty = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ratingPropertySetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Rating Property")
|
||||
.setDesc("Property used to track the rating of a book.")
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FieldSuggest(this.app, cb.inputEl, ["number"]);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
|
||||
cb.setPlaceholder("rating")
|
||||
.setValue(this.plugin.settings.ratingProperty)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ratingProperty = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
endDatePropertySetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("End Date Property")
|
||||
.setDesc("Property used to track the end date of reading a book.")
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FieldSuggest(this.app, cb.inputEl, ["date"]);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
|
||||
cb.setPlaceholder("endDate")
|
||||
.setValue(this.plugin.settings.endDateProperty)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.endDateProperty = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
startDatePropertySetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Start Date Property")
|
||||
.setDesc("Property used to track the start date of reading a book.")
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FieldSuggest(this.app, cb.inputEl, ["date"]);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
|
||||
cb.setPlaceholder("startDate")
|
||||
.setValue(this.plugin.settings.startDateProperty)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.startDateProperty = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
statusPropertySetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Status Property")
|
||||
.setDesc("Property used to track the reading status of a book.")
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FieldSuggest(this.app, cb.inputEl, ["text"]);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
|
||||
cb.setPlaceholder("status")
|
||||
.setValue(this.plugin.settings.statusProperty)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.statusProperty = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
overwriteExistingCoversSetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Overwrite Existing Covers")
|
||||
.setDesc("Overwrite existing book covers when downloading new ones")
|
||||
.addToggle((cb) => {
|
||||
cb.setValue(
|
||||
this.plugin.settings.overwriteExistingCovers
|
||||
).onChange(async (value) => {
|
||||
this.plugin.settings.overwriteExistingCovers = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
groupCoversByFirstLetterSetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Group Covers by First Letter")
|
||||
.setDesc(
|
||||
"Organize downloaded book covers into subdirectories based on the first letter of the book title"
|
||||
)
|
||||
.addToggle((cb) => {
|
||||
cb.setValue(
|
||||
this.plugin.settings.groupCoversByFirstLetter
|
||||
).onChange(async (value) => {
|
||||
this.plugin.settings.groupCoversByFirstLetter = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
coverDirectorySetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Cover Directory")
|
||||
.setDesc(
|
||||
"Select the directory where downloaded book covers will be stored"
|
||||
)
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
|
||||
cb.setPlaceholder("images/covers")
|
||||
.setValue(this.plugin.settings.coverDirectory)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.coverDirectory = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
downloadCoversSetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Download Covers")
|
||||
.setDesc(
|
||||
"Automatically download book covers when creating new entries"
|
||||
)
|
||||
.addToggle((cb) => {
|
||||
cb.setValue(this.plugin.settings.downloadCovers).onChange(
|
||||
async (value) => {
|
||||
this.plugin.settings.downloadCovers = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fileNameFormatSetting() {
|
||||
const fileNameFormatDesc = document.createDocumentFragment();
|
||||
fileNameFormatDesc.createDiv({
|
||||
text: "Format for the file name of new book entries.",
|
||||
this.component = mount(BookTrackerSettingTabView, {
|
||||
target: this.containerEl,
|
||||
props: { plugin: this.plugin },
|
||||
});
|
||||
fileNameFormatDesc.createDiv({
|
||||
text: "Use {{title}} and {{authors}} as placeholders.",
|
||||
});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName("File Name Format")
|
||||
.setDesc(fileNameFormatDesc)
|
||||
.addText((cb) => {
|
||||
cb.setPlaceholder("{{title}} - {{authors}}")
|
||||
.setValue(this.plugin.settings.fileNameFormat)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.fileNameFormat = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tbrDirectorySetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("To Be Read Directory")
|
||||
.setDesc(
|
||||
"Select the directory where new book entries will be created"
|
||||
)
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
const { containerEl } = this;
|
||||
hide(): void {
|
||||
super.hide();
|
||||
|
||||
cb.setPlaceholder("books/tbr")
|
||||
.setValue(this.plugin.settings.tbrDirectory)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.tbrDirectory = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
templateFileSetting() {
|
||||
return new Setting(this.containerEl)
|
||||
.setName("Template File")
|
||||
.setDesc("Select the template file to use for new book entries")
|
||||
.addSearch((cb) => {
|
||||
try {
|
||||
new FileSuggest(this.app, cb.inputEl);
|
||||
} catch {
|
||||
// If the suggest fails, we can just ignore it.
|
||||
// This might happen if the plugin is not fully loaded yet.
|
||||
}
|
||||
|
||||
cb.setPlaceholder("templates/book-template")
|
||||
.setValue(this.plugin.settings.templateFile)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.templateFile = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
if (this.component) {
|
||||
unmount(this.component);
|
||||
this.component = undefined;
|
||||
}
|
||||
this.containerEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
<script lang="ts">
|
||||
import Header from "@ui/components/setting/Header.svelte";
|
||||
import FileSuggestItem from "@ui/components/setting/FileSuggestItem.svelte";
|
||||
import FolderSuggestItem from "@ui/components/setting/FolderSuggestItem.svelte";
|
||||
import TextInputItem from "@ui/components/setting/TextInputItem.svelte";
|
||||
import FieldSuggestItem from "@ui/components/setting/FieldSuggestItem.svelte";
|
||||
import ToggleItem from "@ui/components/setting/ToggleItem.svelte";
|
||||
import type BookTrackerPlugin from "@src/main";
|
||||
import { createSettingsStore } from "./store";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type Props = {
|
||||
plugin: BookTrackerPlugin;
|
||||
};
|
||||
|
||||
const { plugin }: Props = $props();
|
||||
const { app } = plugin;
|
||||
|
||||
const settings = createSettingsStore(plugin);
|
||||
|
||||
onMount(async () => {
|
||||
await settings.load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="obt-settings">
|
||||
<Header title="Book Creation Settings" />
|
||||
<FileSuggestItem
|
||||
{app}
|
||||
id="template-file"
|
||||
name="Template File"
|
||||
description="Select the template file to use for new book entries"
|
||||
bind:value={$settings.templateFile}
|
||||
/>
|
||||
<FolderSuggestItem
|
||||
{app}
|
||||
id="tbr-folder"
|
||||
name="To Be Read Folder"
|
||||
description="Select the folder to use for To Be Read entries"
|
||||
bind:value={$settings.tbrFolder}
|
||||
/>
|
||||
<TextInputItem
|
||||
id="file-name-format"
|
||||
name="File name Format"
|
||||
description={`Format for the file name of new book entries.
|
||||
Use {{title}} and {{authors}} as placeholders.`}
|
||||
bind:value={$settings.fileNameFormat}
|
||||
/>
|
||||
|
||||
<Header title="Cover Download Settings" />
|
||||
<ToggleItem
|
||||
id="download-covers"
|
||||
name="Download Covers"
|
||||
description="Automatically download book covers when creating new entries"
|
||||
bind:checked={$settings.downloadCovers}
|
||||
/>
|
||||
<FolderSuggestItem
|
||||
{app}
|
||||
id="cover-folder"
|
||||
name="Cover Folder"
|
||||
description="Select the folder to download covers to"
|
||||
bind:value={$settings.coverFolder}
|
||||
/>
|
||||
<ToggleItem
|
||||
id="group-covers"
|
||||
name="Group Covers by First Letter"
|
||||
description="Organize downloaded book covers into folders based on the first letter of the book title"
|
||||
bind:checked={$settings.groupCoversByFirstLetter}
|
||||
/>
|
||||
<ToggleItem
|
||||
id="overwrite-covers"
|
||||
name="Overwrite Existing Covers"
|
||||
description="Overwrite existing covers when downloading new ones"
|
||||
bind:checked={$settings.overwriteExistingCovers}
|
||||
/>
|
||||
|
||||
<Header title="Reading Progress Settings" />
|
||||
<FieldSuggestItem
|
||||
{app}
|
||||
id="status-field"
|
||||
name="Status Field"
|
||||
description="Select the folder to use for To Be Read entries"
|
||||
bind:value={$settings.statusProperty}
|
||||
accepts={["text"]}
|
||||
/>
|
||||
<FieldSuggestItem
|
||||
{app}
|
||||
id="start-date-field"
|
||||
name="Start Date Field"
|
||||
description="Select the field to use for start date"
|
||||
bind:value={$settings.startDateProperty}
|
||||
accepts={["date"]}
|
||||
/>
|
||||
<FieldSuggestItem
|
||||
{app}
|
||||
id="end-date-field"
|
||||
name="End Date Field"
|
||||
description="Select the field to use for end date"
|
||||
bind:value={$settings.endDateProperty}
|
||||
accepts={["date"]}
|
||||
/>
|
||||
<FieldSuggestItem
|
||||
{app}
|
||||
id="rating-field"
|
||||
name="Rating Field"
|
||||
description="Select the field to use for rating"
|
||||
bind:value={$settings.ratingProperty}
|
||||
accepts={["number"]}
|
||||
/>
|
||||
<FieldSuggestItem
|
||||
{app}
|
||||
id="page-count-field"
|
||||
name="Page Count Field"
|
||||
description="Select the field to use for page count"
|
||||
bind:value={$settings.pageCountProperty}
|
||||
accepts={["number"]}
|
||||
/>
|
||||
</div>
|
|
@ -1,2 +1,5 @@
|
|||
export { BookTrackerSettingTab } from "./BookTrackerSettingTab";
|
||||
export { type BookTrackerPluginSettings, DEFAULT_SETTINGS } from "./types";
|
||||
export {
|
||||
type BookTrackerSettings as BookTrackerPluginSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
} from "./types";
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import type BookTrackerPlugin from "@src/main";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { type BookTrackerSettings, DEFAULT_SETTINGS } from "./types";
|
||||
|
||||
type SettingsStore = Writable<BookTrackerSettings> & {
|
||||
load: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function createSettingsStore(plugin: BookTrackerPlugin): SettingsStore {
|
||||
const { subscribe, set, update } =
|
||||
writable<BookTrackerSettings>(DEFAULT_SETTINGS);
|
||||
|
||||
async function load() {
|
||||
const settings = await plugin.loadData();
|
||||
|
||||
update((currentSettings) => {
|
||||
return {
|
||||
...currentSettings,
|
||||
...settings,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
subscribe((settings) => {
|
||||
if (settings === DEFAULT_SETTINGS) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.settings = settings;
|
||||
plugin.saveSettings();
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
update,
|
||||
load,
|
||||
};
|
||||
}
|
|
@ -1,31 +1,29 @@
|
|||
export interface BookTrackerPluginSettings {
|
||||
export interface BookTrackerSettings {
|
||||
templateFile: string;
|
||||
tbrDirectory: string;
|
||||
tbrFolder: string;
|
||||
fileNameFormat: string;
|
||||
downloadCovers: boolean;
|
||||
coverDirectory: string;
|
||||
coverFolder: string;
|
||||
groupCoversByFirstLetter: boolean;
|
||||
overwriteExistingCovers: boolean;
|
||||
statusProperty: string;
|
||||
startDateProperty: string;
|
||||
endDateProperty: string;
|
||||
ratingProperty: string;
|
||||
pageLengthProperty: string;
|
||||
readingLogDirectory: string;
|
||||
pageCountProperty: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: BookTrackerPluginSettings = {
|
||||
export const DEFAULT_SETTINGS: BookTrackerSettings = {
|
||||
templateFile: "",
|
||||
tbrDirectory: "books/tbr",
|
||||
tbrFolder: "books/tbr",
|
||||
fileNameFormat: "{{title}} - {{authors}}",
|
||||
downloadCovers: false,
|
||||
coverDirectory: "images/covers",
|
||||
coverFolder: "images/covers",
|
||||
groupCoversByFirstLetter: true,
|
||||
overwriteExistingCovers: false,
|
||||
statusProperty: "status",
|
||||
startDateProperty: "startDate",
|
||||
endDateProperty: "endDate",
|
||||
ratingProperty: "rating",
|
||||
pageLengthProperty: "pageLength",
|
||||
readingLogDirectory: "reading-logs",
|
||||
pageCountProperty: "pageCount",
|
||||
};
|
||||
|
|
|
@ -57,7 +57,7 @@ export class ReadingLog {
|
|||
|
||||
private sortEntries() {
|
||||
this.entries = this.entries.sort(
|
||||
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
||||
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ export class ReadingLog {
|
|||
public async addEntry(
|
||||
book: string,
|
||||
pageEnded: number,
|
||||
pageLength: number
|
||||
pageCount: number
|
||||
): Promise<void> {
|
||||
const latestEntry = this.getLatestEntry(book);
|
||||
|
||||
|
@ -93,7 +93,7 @@ export class ReadingLog {
|
|||
? pageEnded - latestEntry.pagesReadTotal
|
||||
: pageEnded,
|
||||
pagesReadTotal: pageEnded,
|
||||
pagesRemaining: pageLength - pageEnded,
|
||||
pagesRemaining: pageCount - pageEnded,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue