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",
"name": "Book Tracker",
"version": "1.6.1",
"version": "1.7.1",
"minAppVersion": "0.15.0",
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
"author": "FiFiTiDo",

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { ComponentProps } from "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"> & {
id: string;
@ -13,6 +13,6 @@
<Item {name} {description}>
{#snippet control()}
<FileSuggest {id} asString bind:value />
<FileSuggest {id} bind:value />
{/snippet}
</Item>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { ComponentProps } from "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"> & {
id: string;
@ -13,6 +13,6 @@
<Item {name} {description}>
{#snippet control()}
<FolderSuggest {id} asString bind:value />
<FolderSuggest {id} bind:value />
{/snippet}
</Item>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { ComponentProps } from "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"> & {
id: string;
@ -20,6 +20,6 @@
<Item {name} {description}>
{#snippet control()}
<PropertySuggest {id} asString bind:value {accepts} />
<PropertySuggest {id} bind:value {accepts} />
{/snippet}
</Item>

View File

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

View File

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

View File

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

View File

@ -170,35 +170,29 @@
<div class="obt-settings">
<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
id="tbr-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}
/>
<FolderSuggestItem
id="read-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}
/>
<ToggleItem
id="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}
/>
<Header title="Book Creation" />
<FileSuggestItem
id="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}
/>
<TextInputItem

View File

@ -1,5 +1,4 @@
export interface BookTrackerSettings {
bookFolder: string;
tbrFolder: string;
readBooksFolder: string;
organizeReadBooks: boolean;
@ -34,7 +33,6 @@ export interface BookTrackerSettings {
}
export const DEFAULT_SETTINGS: BookTrackerSettings = {
bookFolder: "books",
tbrFolder: "books/tbr",
readBooksFolder: "books/read",
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(...)`.
@ -52,3 +52,7 @@ export async function mkdirRecursive(
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.5.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"
}