Fix images, handlebars escaping, and organize covers by title sort value

This commit is contained in:
Evan Fiordeliso 2025-08-01 12:42:44 -04:00
parent d5e4810d8f
commit cb0c66a48b
10 changed files with 80 additions and 37 deletions

View File

@ -1,7 +1,7 @@
{ {
"id": "obsidian-book-tracker", "id": "obsidian-book-tracker",
"name": "Book Tracker", "name": "Book Tracker",
"version": "1.4.1", "version": "1.4.2",
"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.4.1", "version": "1.4.2",
"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

@ -10,7 +10,7 @@ import {
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
BookTrackerSettingTab, BookTrackerSettingTab,
} from "@ui/settings"; } from "@ui/settings";
import { Templater } from "@utils/Templater"; import { safeString, Templater } from "@utils/Templater";
import { CONTENT_TYPE_EXTENSIONS } from "./const"; import { CONTENT_TYPE_EXTENSIONS } from "./const";
import { Storage } from "@utils/Storage"; import { Storage } from "@utils/Storage";
import { ReadingLog } from "@utils/ReadingLog"; import { ReadingLog } from "@utils/ReadingLog";
@ -34,6 +34,7 @@ import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/Readi
import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock"; import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock";
import moment from "@external/moment"; import moment from "@external/moment";
import { compressImage } from "@utils/image"; import { compressImage } from "@utils/image";
import { titleSortValue } from "@utils/text";
export default class BookTrackerPlugin extends Plugin { export default class BookTrackerPlugin extends Plugin {
public settings: BookTrackerPluginSettings; public settings: BookTrackerPluginSettings;
@ -112,14 +113,14 @@ export default class BookTrackerPlugin extends Plugin {
): Promise<TFile> { ): Promise<TFile> {
const response = await requestUrl(coverImageUrl); const response = await requestUrl(coverImageUrl);
const contentType = response.headers["content-type"]; const contentType = response.headers["content-type"];
const extension = CONTENT_TYPE_EXTENSIONS[contentType || ""] || "";
if (extension === "") { const urlExtension = coverImageUrl.split(".").pop();
throw new Error("Unsupported content type: " + contentType); const extension =
} CONTENT_TYPE_EXTENSIONS[contentType || ""] ?? urlExtension ?? "jpg";
let filePath = this.settings.coverFolder + "/"; let filePath = this.settings.coverFolder + "/";
if (this.settings.groupCoversByFirstLetter) { if (this.settings.groupCoversByFirstLetter) {
let groupName = fileName.charAt(0).toUpperCase(); let groupName = titleSortValue(fileName).charAt(0).toUpperCase();
if (!/^[A-Z]$/.test(groupName)) { if (!/^[A-Z]$/.test(groupName)) {
groupName = "#"; groupName = "#";
} }
@ -162,19 +163,43 @@ export default class BookTrackerPlugin extends Plugin {
async createEntry(book: Book): Promise<void> { async createEntry(book: Book): Promise<void> {
const fileName = this.templater const fileName = this.templater
.renderTemplate(this.settings.fileNameFormat, { .renderTemplate(this.settings.fileNameFormat, {
title: book.title, title: safeString(book.title),
authors: book.authors.map((a) => a.name).join(", "), authors: safeString(book.authors.map((a) => a.name).join(", ")),
}) })
.replace(/[/:*?<>|""]/g, ""); .replace(/[/:*?<>|""]/g, "");
const data: Record<string, unknown> = { book }; // Wrap strings as safe strings to avoid Handlebars escaping
const data: Record<string, unknown> = {
book: {
title: safeString(book.title),
subtitle: safeString(book.subtitle),
description: book.description,
authors: book.authors.map((a) => ({
...a,
name: safeString(a.name),
})),
series: book.series
? {
...book.series,
title: safeString(book.series.title),
}
: null,
publisher: safeString(book.publisher),
publishedAt: book.publishedAt,
genres: book.genres.map((g) => safeString(g)),
coverImageUrl: safeString(book.coverImageUrl),
pageCount: book.pageCount,
isbn: safeString(book.isbn),
isbn13: safeString(book.isbn13),
},
};
if (this.settings.downloadCovers && book.coverImageUrl) { if (this.settings.downloadCovers && book.coverImageUrl) {
const coverImageFile = await this.downloadCoverImage( const coverImageFile = await this.downloadCoverImage(
book.coverImageUrl, book.coverImageUrl,
fileName fileName
); );
data.coverImagePath = coverImageFile.path; data.coverImagePath = safeString(coverImageFile.path);
} }
const renderedContent = await this.templater.renderTemplateFile( const renderedContent = await this.templater.renderTemplateFile(
@ -225,7 +250,15 @@ export default class BookTrackerPlugin extends Plugin {
const getDate = (key: string) => { const getDate = (key: string) => {
const value = fm?.[key]; const value = fm?.[key];
if (typeof value === "string" || value instanceof Date) { if (typeof value === "string") {
if (value.includes(" ")) {
return moment(value, "YYYY-MM-DD HH:mm:ss");
} else if (value.includes("T")) {
return moment(value, "YYYY-MM-DDTHH:mm:ss");
} else {
return moment(value, "YYYY-MM-DD");
}
} else if (value instanceof Date) {
return moment(value); return moment(value);
} }
return null; return null;

View File

@ -13,6 +13,7 @@
import DateFilter from "@ui/components/DateFilter.svelte"; import DateFilter from "@ui/components/DateFilter.svelte";
import BookCover from "@ui/components/BookCover.svelte"; import BookCover from "@ui/components/BookCover.svelte";
import { setAppContext } from "@ui/stores/app"; import { setAppContext } from "@ui/stores/app";
import { titleSortValue } from "@utils/text";
const { plugin }: SvelteCodeBlockProps = $props(); const { plugin }: SvelteCodeBlockProps = $props();
setAppContext(plugin.app); setAppContext(plugin.app);
@ -26,25 +27,15 @@
disableMonthFilter: true, disableMonthFilter: true,
}); });
const getSortValue = (value: string) => {
if (value.startsWith("A ")) {
return value.slice(2) + ", A";
} else if (value.startsWith("An ")) {
return value.slice(3) + ", An";
} else if (value.startsWith("The ")) {
return value.slice(4) + ", The";
} else {
return value;
}
};
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const metadata = $derived( const metadata = $derived(
metadataStore.metadata.reduce( metadataStore.metadata.reduce(
(acc, meta) => { (acc, meta) => {
const title = meta.book.title; const title = meta.book.title;
const firstLetter = getSortValue(title).charAt(0).toUpperCase(); const firstLetter = titleSortValue(title)
.charAt(0)
.toUpperCase();
if (!firstLetter.match(/[A-Z]/)) { if (!firstLetter.match(/[A-Z]/)) {
return acc; return acc;

View File

@ -87,16 +87,17 @@
} }
.book-cover { .book-cover {
width: var(--book-cover-width); &,
height: var( & img {
--book-cover-height, width: var(--book-cover-width);
calc(var(--book-cover-width) / (var(--book-cover-aspect-ratio))) height: var(
); --book-cover-height,
calc(var(--book-cover-width) / (var(--book-cover-aspect-ratio)))
);
}
img { img {
border-radius: var(--radius-l); border-radius: var(--radius-l);
height: 100%;
width: 100%;
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
} }

View File

@ -7,6 +7,7 @@
import OpenFileLink from "./OpenFileLink.svelte"; import OpenFileLink from "./OpenFileLink.svelte";
import BookCover from "./BookCover.svelte"; import BookCover from "./BookCover.svelte";
import { getReadingLogContext } from "@ui/stores/reading-log.svelte"; import { getReadingLogContext } from "@ui/stores/reading-log.svelte";
import { Platform } from "obsidian";
interface Props { interface Props {
settings: ShelfSettings; settings: ShelfSettings;
@ -21,7 +22,10 @@
<div class="book-details-list"> <div class="book-details-list">
{#each metadataStore.metadata as meta} {#each metadataStore.metadata as meta}
<div class="book-details"> <div class="book-details">
<BookCover book={meta.book} /> <BookCover
book={meta.book}
size={Platform.isMobile ? "20rem" : "fill"}
/>
<div class="book-info"> <div class="book-info">
<OpenFileLink file={meta.file}> <OpenFileLink file={meta.file}>
<h2 class="book-title"> <h2 class="book-title">

View File

@ -25,6 +25,10 @@ Handlebars.registerHelper("indent", (text: string, indent = " ") => {
); );
}); });
export function safeString(str: string) {
return new Handlebars.SafeString(str);
}
export class Templater { export class Templater {
public constructor(private readonly app: App) {} public constructor(private readonly app: App) {}

View File

@ -47,8 +47,6 @@ export async function compressImage(
} }
} }
console.log(width, height);
canvas.width = width!; canvas.width = width!;
canvas.height = height!; canvas.height = height!;

11
src/utils/text.ts Normal file
View File

@ -0,0 +1,11 @@
export function titleSortValue(title: string): string {
if (title.toLowerCase().startsWith("a ")) {
return title.slice(2) + ", A";
} else if (title.toLowerCase().startsWith("an ")) {
return title.slice(3) + ", An";
} else if (title.toLowerCase().startsWith("the ")) {
return title.slice(4) + ", The";
} else {
return title;
}
}

View File

@ -6,5 +6,6 @@
"1.3.1": "0.15.0", "1.3.1": "0.15.0",
"1.3.2": "0.15.0", "1.3.2": "0.15.0",
"1.4.0": "0.15.0", "1.4.0": "0.15.0",
"1.4.1": "0.15.0" "1.4.1": "0.15.0",
"1.4.2": "0.15.0"
} }