Compare commits

...

2 Commits
v1.6.1 ... main

18 changed files with 127 additions and 83 deletions

View File

@ -1,7 +1,7 @@
{ {
"id": "obsidian-book-tracker", "id": "obsidian-book-tracker",
"name": "Book Tracker", "name": "Book Tracker",
"version": "1.6.1", "version": "1.7.1",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.", "description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
"author": "FiFiTiDo", "author": "FiFiTiDo",

View File

@ -1,6 +1,6 @@
{ {
"name": "obsidian-book-tracker", "name": "obsidian-book-tracker",
"version": "1.6.1", "version": "1.7.1",
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.", "description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@ -0,0 +1,11 @@
<script lang="ts">
interface Props {
id?: string;
name?: string;
value?: string;
}
let { id, name, value = $bindable() }: Props = $props();
</script>
<input {id} {name} type="text" bind:value />

View File

@ -0,0 +1,21 @@
<script lang="ts">
interface Props {
id?: string;
name?: string;
checked?: boolean;
}
let { id, name, checked = $bindable() }: Props = $props();
</script>
<div
class="checkbox-container"
class:is-enabled={checked}
onclick={() => (checked = !checked)}
onkeypress={(e) => e.key === "Space" && (checked = !checked)}
role="switch"
aria-checked={checked}
tabindex="0"
>
<input {id} {name} type="checkbox" bind:checked tabindex="0" />
</div>

View File

@ -3,42 +3,39 @@
import TextInputSuggest, { type Item } from "./TextInputSuggest.svelte"; import TextInputSuggest, { type Item } from "./TextInputSuggest.svelte";
import type { StringKeys } from "@utils/types"; import type { StringKeys } from "@utils/types";
import { getAppContext } from "@ui/stores/app"; import { getAppContext } from "@ui/stores/app";
import { isInAnyFolder } from "@utils/fs";
type Props = { type Props = {
id: string; id: string;
asString?: boolean;
property?: StringKeys<TFile>; property?: StringKeys<TFile>;
inFolder?: string; folderFilter?: string[];
value?: TFile | string; value?: string;
disabled?: boolean; disabled?: boolean;
onSelected?: (fileOrPath: TFile | string) => void; onSelected?: (propertyValue: string) => void;
}; };
let { let {
id, id,
asString,
property = "path", property = "path",
inFolder, folderFilter,
value = $bindable(), value = $bindable(),
disabled, disabled,
onSelected, onSelected,
}: Props = $props(); }: Props = $props();
const app = getAppContext(); const app = getAppContext();
let items: Item<TFile | string>[] = $state([]); let items: Item<string>[] = $state([]);
function handleChange(query: string) { function handleChange(query: string) {
items = app.vault items = app.vault
.getMarkdownFiles() .getMarkdownFiles()
.filter( .filter(
(f) => (f) =>
(inFolder === undefined || f.path.startsWith(inFolder)) && (folderFilter === undefined ||
isInAnyFolder(f, folderFilter)) &&
f[property].toLowerCase().includes(query.toLowerCase()), f[property].toLowerCase().includes(query.toLowerCase()),
) )
.map((f) => ({ .map((f) => f[property]);
text: f[property],
value: asString ? f[property] : f,
}));
} }
</script> </script>

View File

@ -3,22 +3,21 @@
import TextInputSuggest, { type Item } from "./TextInputSuggest.svelte"; import TextInputSuggest, { type Item } from "./TextInputSuggest.svelte";
import type { StringKeys } from "@utils/types"; import type { StringKeys } from "@utils/types";
import { getAppContext } from "@ui/stores/app"; import { getAppContext } from "@ui/stores/app";
import { isInAnyFolder } from "@utils/fs";
type Props = { type Props = {
id: string; id: string;
asString?: boolean;
property?: StringKeys<TFolder>; property?: StringKeys<TFolder>;
inFolder?: string; folderFilter?: string[];
value?: TFolder | string; value?: string;
disabled?: boolean; disabled?: boolean;
onSelected?: (folderOrPath: TFolder | string) => void; onSelected?: (propertyValue: string) => void;
}; };
let { let {
id, id,
asString,
property = "path", property = "path",
inFolder, folderFilter,
value = $bindable(), value = $bindable(),
disabled, disabled,
onSelected, onSelected,
@ -32,13 +31,11 @@
.getAllFolders() .getAllFolders()
.filter( .filter(
(f) => (f) =>
(inFolder === undefined || f.path.startsWith(inFolder)) && (folderFilter === undefined ||
isInAnyFolder(f, folderFilter)) &&
f[property].toLowerCase().includes(query.toLowerCase()), f[property].toLowerCase().includes(query.toLowerCase()),
) )
.map((f) => ({ .map((f) => f[property]);
text: f[property],
value: asString ? f[property] : f,
}));
} }
</script> </script>

View File

@ -12,16 +12,14 @@
type Props = { type Props = {
id: string; id: string;
asString?: boolean; value?: string;
value?: Property | string;
accepts?: string[]; accepts?: string[];
disabled?: boolean; disabled?: boolean;
onSelected?: (propertyOrName: Property | string) => void; onSelected?: (propertyName: string) => void;
}; };
let { let {
id, id,
asString,
value = $bindable(), value = $bindable(),
accepts, accepts,
disabled, disabled,
@ -29,7 +27,7 @@
}: Props = $props(); }: Props = $props();
const app = getAppContext(); const app = getAppContext();
let items: Item<Property | string>[] = $state([]); let items: Item<string>[] = $state([]);
async function handleChange(query: string) { async function handleChange(query: string) {
const typesContent = await app.vault.adapter.read( const typesContent = await app.vault.adapter.read(
@ -45,10 +43,7 @@
return name.toLowerCase().includes(query.toLowerCase()); return name.toLowerCase().includes(query.toLowerCase());
}) })
.map(([name, type]) => ({ .map(([name, _]) => name);
text: name,
value: asString ? name : { name, type },
}));
} }
</script> </script>

View File

@ -1,8 +1,10 @@
<script lang="ts" module> <script lang="ts" module>
export type Item<T> = { export type Item<T> =
text: string; | {
label: string;
value: T; value: T;
}; }
| (T extends string ? T : never);
</script> </script>
<script lang="ts"> <script lang="ts">
@ -26,7 +28,7 @@
let { let {
id, id,
items, items: itemsProp,
value = $bindable(), value = $bindable(),
loading = false, loading = false,
suggestion, suggestion,
@ -35,6 +37,12 @@
onSelected, onSelected,
}: Props = $props(); }: Props = $props();
const items = $derived(
itemsProp.map((item) =>
typeof item === "string" ? { label: item, value: item } : item,
),
);
let query = $state(""); let query = $state("");
let expanded = $state(false); let expanded = $state(false);
let selectedIndex = $state(0); let selectedIndex = $state(0);
@ -53,7 +61,7 @@
const idx = findIndex(value); const idx = findIndex(value);
if (idx !== -1) { if (idx !== -1) {
const item = items[idx]; const item = items[idx];
query = item.text; query = item.label;
selectedIndex = idx; selectedIndex = idx;
} }
}); });
@ -99,7 +107,7 @@
async function selectItem(index: number) { async function selectItem(index: number) {
const item = items[index]; const item = items[index];
if (!item) return; if (!item) return;
const { text: newQuery, value: newValue } = item; const { label: newQuery, value: newValue } = item;
selectedIndex = index; selectedIndex = index;
expanded = false; expanded = false;
query = newQuery; query = newQuery;
@ -195,7 +203,7 @@
{#if suggestion} {#if suggestion}
{@render suggestion(item)} {@render suggestion(item)}
{:else} {:else}
{item.text} {item.label}
{/if} {/if}
</li> </li>
{/each} {/each}

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { ComponentProps } from "svelte"; import type { ComponentProps } from "svelte";
import Item from "./Item.svelte"; import Item from "./Item.svelte";
import FileSuggest from "../suggesters/FileSuggest.svelte"; import FileSuggest from "../form/suggesters/FileSuggest.svelte";
type Props = Omit<ComponentProps<typeof Item>, "control"> & { type Props = Omit<ComponentProps<typeof Item>, "control"> & {
id: string; id: string;
@ -13,6 +13,6 @@
<Item {name} {description}> <Item {name} {description}>
{#snippet control()} {#snippet control()}
<FileSuggest {id} asString bind:value /> <FileSuggest {id} bind:value />
{/snippet} {/snippet}
</Item> </Item>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { ComponentProps } from "svelte"; import type { ComponentProps } from "svelte";
import Item from "./Item.svelte"; import Item from "./Item.svelte";
import FolderSuggest from "../suggesters/FolderSuggest.svelte"; import FolderSuggest from "../form/suggesters/FolderSuggest.svelte";
type Props = Omit<ComponentProps<typeof Item>, "control"> & { type Props = Omit<ComponentProps<typeof Item>, "control"> & {
id: string; id: string;
@ -13,6 +13,6 @@
<Item {name} {description}> <Item {name} {description}>
{#snippet control()} {#snippet control()}
<FolderSuggest {id} asString bind:value /> <FolderSuggest {id} bind:value />
{/snippet} {/snippet}
</Item> </Item>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { ComponentProps } from "svelte"; import type { ComponentProps } from "svelte";
import Item from "./Item.svelte"; import Item from "./Item.svelte";
import PropertySuggest from "../suggesters/PropertySuggest.svelte"; import PropertySuggest from "../form/suggesters/PropertySuggest.svelte";
type Props = Omit<ComponentProps<typeof Item>, "control"> & { type Props = Omit<ComponentProps<typeof Item>, "control"> & {
id: string; id: string;
@ -20,6 +20,6 @@
<Item {name} {description}> <Item {name} {description}>
{#snippet control()} {#snippet control()}
<PropertySuggest {id} asString bind:value {accepts} /> <PropertySuggest {id} bind:value {accepts} />
{/snippet} {/snippet}
</Item> </Item>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { ComponentProps } from "svelte"; import type { ComponentProps } from "svelte";
import Item from "./Item.svelte"; import Item from "./Item.svelte";
import TextControl from "../form/TextControl.svelte";
type Props = Omit<ComponentProps<typeof Item>, "control"> & { type Props = Omit<ComponentProps<typeof Item>, "control"> & {
id?: string; id?: string;
@ -12,6 +13,6 @@
<Item {name} {description}> <Item {name} {description}>
{#snippet control()} {#snippet control()}
<input {id} type="text" bind:value /> <TextControl {id} bind:value />
{/snippet} {/snippet}
</Item> </Item>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { ComponentProps } from "svelte"; import type { ComponentProps } from "svelte";
import Item from "./Item.svelte"; import Item from "./Item.svelte";
import ToggleControl from "../form/ToggleControl.svelte";
type Props = Omit<ComponentProps<typeof Item>, "control"> & { type Props = Omit<ComponentProps<typeof Item>, "control"> & {
id?: string; id?: string;
@ -12,15 +13,6 @@
<Item {name} {description}> <Item {name} {description}>
{#snippet control()} {#snippet control()}
<!-- svelte-ignore a11y_no_static_element_interactions --> <ToggleControl {id} bind:checked />
<!-- 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} {/snippet}
</Item> </Item>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type BookTrackerPlugin from "@src/main"; import type BookTrackerPlugin from "@src/main";
import type { ReadingLogEntry } from "@utils/ReadingLog"; import type { ReadingLogEntry } from "@utils/ReadingLog";
import FileSuggest from "@ui/components/suggesters/FileSuggest.svelte"; import FileSuggest from "@ui/components/form/suggesters/FileSuggest.svelte";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { createPrevious } from "@ui/stores/previous.svelte"; import { createPrevious } from "@ui/stores/previous.svelte";
import { createMetadata } from "@ui/stores/metadata.svelte"; import { createMetadata } from "@ui/stores/metadata.svelte";
@ -16,6 +16,7 @@
setReadingLogContext, setReadingLogContext,
} from "@ui/stores/reading-log.svelte"; } from "@ui/stores/reading-log.svelte";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import ToggleControl from "@ui/components/form/ToggleControl.svelte";
const INPUT_DATETIME_FORMAT = "YYYY-MM-DDTHH:mm"; const INPUT_DATETIME_FORMAT = "YYYY-MM-DDTHH:mm";
@ -43,13 +44,24 @@
const bookMetadata = $derived( const bookMetadata = $derived(
metadataStore.metadata.find((m) => m.file.basename === book), metadataStore.metadata.find((m) => m.file.basename === book),
); );
const lastEntryIndex = $derived( const previousEntry = $derived.by(() => {
readingLogStore.entries.findLastIndex((e) => e.book === book), const index = readingLogStore.entries.findIndex(
); (e) => e.id === entry?.id,
const lastEntry = $derived(
lastEntryIndex !== -1 ? readingLogStore.entries[lastEntryIndex] : null,
); );
let data = readingLogStore.entries;
if (index !== -1) {
data = data.slice(0, index);
}
const previousEntryIndex = data.findLastIndex((e) => e.book === book);
return previousEntryIndex !== -1
? readingLogStore.entries[previousEntryIndex]
: null;
});
let syncPageCounts = $state(true);
const pageCount = $derived(bookMetadata?.book.pageCount ?? 0); const pageCount = $derived(bookMetadata?.book.pageCount ?? 0);
let pagesRead = $state(entry?.pagesRead ?? 0); let pagesRead = $state(entry?.pagesRead ?? 0);
const pagesReadPrev = createPrevious(() => pagesRead); const pagesReadPrev = createPrevious(() => pagesRead);
@ -61,20 +73,24 @@
); );
$effect(() => { $effect(() => {
pagesReadTotal = (lastEntry?.pagesReadTotal ?? 0) + pagesRead; if (!syncPageCounts) return;
pagesReadTotal = (previousEntry?.pagesReadTotal ?? 0) + pagesRead;
}); });
$effect(() => { $effect(() => {
if (!syncPageCounts) return;
const diff = pagesRead - (pagesReadPrev.value ?? 0); const diff = pagesRead - (pagesReadPrev.value ?? 0);
pagesRead = pagesRead; pagesRead = pagesRead;
pagesReadTotal = pagesReadTotal + diff; pagesReadTotal = pagesReadTotal + diff;
}); });
$effect(() => { $effect(() => {
if (!syncPageCounts) return;
pagesRemaining = pageCount - pagesReadTotal; pagesRemaining = pageCount - pagesReadTotal;
}); });
$effect(() => { $effect(() => {
if (!syncPageCounts) return;
pagesReadTotal = Math.max(pagesReadTotal, pagesRead); pagesReadTotal = Math.max(pagesReadTotal, pagesRead);
}); });
@ -108,9 +124,11 @@
<label for="book">Book</label> <label for="book">Book</label>
<FileSuggest <FileSuggest
id="book" id="book"
asString
property="basename" property="basename"
inFolder={plugin.settings.bookFolder} folderFilter={[
plugin.settings.tbrFolder,
plugin.settings.readBooksFolder,
]}
bind:value={book} bind:value={book}
/> />
<label for="pagesRead">Pages Read</label> <label for="pagesRead">Pages Read</label>
@ -136,6 +154,12 @@
id="pagesRemaining" id="pagesRemaining"
bind:value={pagesRemaining} bind:value={pagesRemaining}
/> />
<label for="syncPageCounts">Sync Page Counts</label>
<ToggleControl
name="syncPageCounts"
id="syncPageCounts"
bind:checked={syncPageCounts}
/>
<label for="createdAt">Created At</label> <label for="createdAt">Created At</label>
<input <input
type="datetime-local" type="datetime-local"

View File

@ -170,35 +170,29 @@
<div class="obt-settings"> <div class="obt-settings">
<Header title="Folders" /> <Header title="Folders" />
<FolderSuggestItem
id="book-folder"
name="Book Folder"
description="Select the folder where book entries are stored."
bind:value={settingsStore.settings.bookFolder}
/>
<FolderSuggestItem <FolderSuggestItem
id="tbr-folder" id="tbr-folder"
name="To Be Read Folder" name="To Be Read Folder"
description="Select the folder to use for To Be Read entries" description="The folder to use for To Be Read or Currently Reading book entries."
bind:value={settingsStore.settings.tbrFolder} bind:value={settingsStore.settings.tbrFolder}
/> />
<FolderSuggestItem <FolderSuggestItem
id="read-folder" id="read-folder"
name="Read Books Folder" name="Read Books Folder"
description="Select the folder to use for Read entries." description="The folder to use for Read book entries."
bind:value={settingsStore.settings.readBooksFolder} bind:value={settingsStore.settings.readBooksFolder}
/> />
<ToggleItem <ToggleItem
id="organize-read-books" id="organize-read-books"
name="Organize Read Books" name="Organize Read Books"
description="Organize read books into folders based on the date read." description="Whether to automatically organize read books into folders, based on the date read, when finishing a book."
bind:checked={settingsStore.settings.organizeReadBooks} bind:checked={settingsStore.settings.organizeReadBooks}
/> />
<Header title="Book Creation" /> <Header title="Book Creation" />
<FileSuggestItem <FileSuggestItem
id="template-file" id="template-file"
name="Template File" name="Template File"
description="Select the template file to use for new book entries." description="The template file to use when creating new book entries."
bind:value={settingsStore.settings.templateFile} bind:value={settingsStore.settings.templateFile}
/> />
<TextInputItem <TextInputItem

View File

@ -1,5 +1,4 @@
export interface BookTrackerSettings { export interface BookTrackerSettings {
bookFolder: string;
tbrFolder: string; tbrFolder: string;
readBooksFolder: string; readBooksFolder: string;
organizeReadBooks: boolean; organizeReadBooks: boolean;
@ -34,7 +33,6 @@ export interface BookTrackerSettings {
} }
export const DEFAULT_SETTINGS: BookTrackerSettings = { export const DEFAULT_SETTINGS: BookTrackerSettings = {
bookFolder: "books",
tbrFolder: "books/tbr", tbrFolder: "books/tbr",
readBooksFolder: "books/read", readBooksFolder: "books/read",
organizeReadBooks: true, organizeReadBooks: true,

View File

@ -1,4 +1,4 @@
import { normalizePath, type Vault } from "obsidian"; import { normalizePath, TAbstractFile, type Vault } from "obsidian";
/** /**
* A simple analog of Node.js's `path.join(...)`. * A simple analog of Node.js's `path.join(...)`.
@ -52,3 +52,7 @@ export async function mkdirRecursive(
await vault.adapter.mkdir(stack.pop()!); await vault.adapter.mkdir(stack.pop()!);
} }
} }
export function isInAnyFolder(file: TAbstractFile, folders: string[]): boolean {
return folders.some((folder) => file.path.startsWith(folder));
}

View File

@ -10,5 +10,7 @@
"1.4.2": "0.15.0", "1.4.2": "0.15.0",
"1.5.0": "0.15.0", "1.5.0": "0.15.0",
"1.6.0": "0.15.0", "1.6.0": "0.15.0",
"1.6.1": "0.15.0" "1.6.1": "0.15.0",
"1.7.0": "0.15.0",
"1.7.1": "0.15.0"
} }