generated from tpl/obsidian-sample-plugin
Move goodreads functions to class to expose via the plugin api
This commit is contained in:
parent
c58f1c4bf5
commit
ad990dae8a
|
@ -1,4 +1,4 @@
|
|||
import { getBookByLegacyId, type SearchResult } from "@data-sources/goodreads";
|
||||
import type { Goodreads, SearchResult } from "@data-sources/Goodreads";
|
||||
import { GoodreadsSearchModal, GoodreadsSearchSuggestModal } from "@ui/modals";
|
||||
import { App, Notice } from "obsidian";
|
||||
import { Command } from "./Command";
|
||||
|
@ -7,6 +7,7 @@ import type { Book } from "@src/types";
|
|||
export class SearchGoodreadsCommand extends Command {
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
private readonly goodreads: Goodreads,
|
||||
private readonly cb: (book: Book) => void
|
||||
) {
|
||||
super("search-goodreads", "Search Goodreads");
|
||||
|
@ -15,7 +16,10 @@ export class SearchGoodreadsCommand extends Command {
|
|||
async callback() {
|
||||
let results: SearchResult[];
|
||||
try {
|
||||
results = await GoodreadsSearchModal.createAndOpen(this.app);
|
||||
results = await GoodreadsSearchModal.createAndOpen(
|
||||
this.app,
|
||||
this.goodreads
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to search Goodreads:", error);
|
||||
new Notice(
|
||||
|
@ -35,7 +39,9 @@ export class SearchGoodreadsCommand extends Command {
|
|||
|
||||
let book: Book;
|
||||
try {
|
||||
book = await getBookByLegacyId(selectedResult.legacyId);
|
||||
book = await this.goodreads.getBookByLegacyId(
|
||||
selectedResult.legacyId
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to get book:", error);
|
||||
new Notice("Failed to get book. Check console for details.");
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
import { requestUrl } from "obsidian";
|
||||
import type {
|
||||
Author,
|
||||
Book as OutputBook,
|
||||
Series as OutputSeries,
|
||||
} from "../types";
|
||||
|
||||
interface Ref {
|
||||
__ref: string;
|
||||
}
|
||||
|
||||
interface BookContributorEdge {
|
||||
__typename: "BookContributorEdge";
|
||||
node: Ref;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface BookSeries {
|
||||
__typename: "BookSeries";
|
||||
userPosition: string;
|
||||
series: Ref;
|
||||
}
|
||||
|
||||
interface Genre {
|
||||
__typename: "Genre";
|
||||
name: string;
|
||||
webUrl: string;
|
||||
}
|
||||
|
||||
interface BookGenre {
|
||||
__typename: "BookGenre";
|
||||
genre: Genre;
|
||||
}
|
||||
|
||||
interface Language {
|
||||
__typename: "Language";
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface BookDetails {
|
||||
__typename: "BookDetails";
|
||||
asin: string;
|
||||
format: string;
|
||||
numPages: number;
|
||||
publicationTime: number;
|
||||
publisher: string;
|
||||
isbn: string;
|
||||
isbn13: string;
|
||||
language: Language;
|
||||
}
|
||||
|
||||
interface Book {
|
||||
__typename: "Book";
|
||||
id: string;
|
||||
legacyId: number;
|
||||
webUrl: string;
|
||||
title: string;
|
||||
titleComplete: string;
|
||||
description: string;
|
||||
primaryContributorEdge: BookContributorEdge;
|
||||
secondaryContributorEdges: BookContributorEdge[];
|
||||
imageUrl: string;
|
||||
bookSeries: BookSeries[];
|
||||
bookGenres: BookGenre[];
|
||||
details: BookDetails;
|
||||
work: Ref;
|
||||
}
|
||||
|
||||
interface ContributorWorksConnection {
|
||||
__typename: "ContributorWorksConnection";
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
interface ContributorFollowersConnection {
|
||||
__typename: "ContributorFollowersConnection";
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
interface Contributor {
|
||||
__typename: "Contributor";
|
||||
id: string;
|
||||
legacyId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
isGrAuthor: boolean;
|
||||
works: ContributorWorksConnection;
|
||||
profileImageUrl: string;
|
||||
webUrl: string;
|
||||
followers: ContributorFollowersConnection;
|
||||
}
|
||||
|
||||
interface Series {
|
||||
__typename: "Series";
|
||||
id: string;
|
||||
title: string;
|
||||
webUrl: string;
|
||||
}
|
||||
|
||||
interface Work {
|
||||
__typename: "Work";
|
||||
id: string;
|
||||
legacyId: number;
|
||||
bestBook: Ref;
|
||||
// ...Book
|
||||
}
|
||||
|
||||
interface Query {
|
||||
__typename: "Query";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface NextData {
|
||||
props: {
|
||||
pageProps: {
|
||||
apolloState: {
|
||||
ROOT_QUERY: Query;
|
||||
[key: string]: any;
|
||||
};
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
jwtToken: string;
|
||||
dataSource: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
legacyId: number;
|
||||
title: string;
|
||||
authors: string[];
|
||||
avgRating: number;
|
||||
ratingCount: number;
|
||||
publicationYear: number;
|
||||
editionCount: number;
|
||||
coverImageUrl: string;
|
||||
}
|
||||
|
||||
export class Goodreads {
|
||||
async getNextData(legacyId: number): Promise<NextData> {
|
||||
const url = "https://www.goodreads.com/book/show/" + legacyId;
|
||||
const res = await requestUrl({ url });
|
||||
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
||||
let nextDataRaw = doc.getElementById("__NEXT_DATA__")?.textContent;
|
||||
if (typeof nextDataRaw !== "string") {
|
||||
throw new Error("Unable to find next data script in the document.");
|
||||
}
|
||||
const nextData = JSON.parse(nextDataRaw) as NextData;
|
||||
|
||||
return nextData;
|
||||
}
|
||||
|
||||
extractBookFromNextData(nextData: NextData, ref: Ref): OutputBook {
|
||||
const apolloState = nextData.props.pageProps.apolloState;
|
||||
const bookData = apolloState[ref.__ref] as Book;
|
||||
|
||||
const contributorEdges = [
|
||||
bookData.primaryContributorEdge,
|
||||
...bookData.secondaryContributorEdges,
|
||||
];
|
||||
const authors = contributorEdges
|
||||
.filter((edge) => edge.role === "Author")
|
||||
.map<Contributor>((edge) => apolloState[edge.node.__ref])
|
||||
.map<Author>((contributor) => ({
|
||||
id: contributor.id,
|
||||
legacyId: contributor.legacyId,
|
||||
name: contributor.name,
|
||||
description: contributor.description,
|
||||
}));
|
||||
|
||||
let series: OutputSeries | null = null;
|
||||
if (bookData.bookSeries.length > 0) {
|
||||
const bookSeries = bookData.bookSeries[0];
|
||||
const seriesData = apolloState[bookSeries.series.__ref] as Series;
|
||||
series = {
|
||||
title: seriesData.title,
|
||||
position: parseInt(bookSeries.userPosition, 10),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: bookData.title,
|
||||
description: bookData.description,
|
||||
authors,
|
||||
series,
|
||||
publisher: bookData.details.publisher,
|
||||
publishedAt: new Date(bookData.details.publicationTime),
|
||||
genres: bookData.bookGenres.map((genre) => genre.genre.name),
|
||||
coverImageUrl: bookData.imageUrl,
|
||||
pageCount: bookData.details.numPages,
|
||||
isbn: bookData.details.isbn,
|
||||
isbn13: bookData.details.isbn13,
|
||||
};
|
||||
}
|
||||
|
||||
async getBookByLegacyId(legacyId: number): Promise<OutputBook> {
|
||||
const nextData = await this.getNextData(legacyId);
|
||||
const bookRef = nextData.props.pageProps.apolloState.ROOT_QUERY[
|
||||
`getBookByLegacyId({"legacyId":"${legacyId}"})`
|
||||
] as Ref | undefined;
|
||||
if (bookRef === undefined) {
|
||||
throw new Error("Could not find reference for book.");
|
||||
}
|
||||
|
||||
return this.extractBookFromNextData(nextData, bookRef);
|
||||
}
|
||||
|
||||
async search(q: string): Promise<SearchResult[]> {
|
||||
const url =
|
||||
"https://www.goodreads.com/search?q=" + encodeURIComponent(q);
|
||||
const res = await requestUrl({ url });
|
||||
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
||||
|
||||
return Array.from(
|
||||
doc.querySelectorAll(
|
||||
"table.tableList tr[itemtype='http://schema.org/Book']"
|
||||
)
|
||||
).map<SearchResult>((el) => {
|
||||
const legacyId = parseInt(
|
||||
el.querySelector("div.u-anchorTarget")?.id ?? "",
|
||||
10
|
||||
);
|
||||
|
||||
const title =
|
||||
el.querySelector("a.bookTitle")?.textContent?.trim() || "";
|
||||
|
||||
const authors = Array.from(el.querySelectorAll("a.authorName")).map(
|
||||
(a) => a.textContent?.trim() || ""
|
||||
);
|
||||
|
||||
const avgRating = parseFloat(
|
||||
el
|
||||
.querySelector("span.minirating")
|
||||
?.textContent?.match(/(\d+\.\d+) avg rating/)?.[1] ?? "0"
|
||||
);
|
||||
|
||||
const ratingCount = parseInt(
|
||||
el
|
||||
.querySelector("span.minirating")
|
||||
?.textContent?.match(/(\d[\d,]*) ratings/)?.[1] ?? "0",
|
||||
10
|
||||
);
|
||||
|
||||
const publicationYear = parseInt(
|
||||
el
|
||||
.querySelector("span.greyText")
|
||||
?.textContent?.match(/published (\d{4})/)?.[1] ?? "0",
|
||||
10
|
||||
);
|
||||
|
||||
const editionCount = parseInt(
|
||||
el
|
||||
.querySelector("span.greyText")
|
||||
?.textContent?.match(/(\d+) editions/)?.[1] ?? "0",
|
||||
10
|
||||
);
|
||||
|
||||
const coverImageUrl =
|
||||
el.querySelector("img.bookCover")?.getAttribute("src") || "";
|
||||
|
||||
return {
|
||||
legacyId,
|
||||
title,
|
||||
authors,
|
||||
avgRating,
|
||||
ratingCount,
|
||||
publicationYear,
|
||||
editionCount,
|
||||
coverImageUrl,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,272 +0,0 @@
|
|||
import { requestUrl } from "obsidian";
|
||||
import type {
|
||||
Author,
|
||||
Book as OutputBook,
|
||||
Series as OutputSeries,
|
||||
} from "../types";
|
||||
|
||||
interface Ref {
|
||||
__ref: string;
|
||||
}
|
||||
|
||||
interface BookContributorEdge {
|
||||
__typename: "BookContributorEdge";
|
||||
node: Ref;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface BookSeries {
|
||||
__typename: "BookSeries";
|
||||
userPosition: string;
|
||||
series: Ref;
|
||||
}
|
||||
|
||||
interface Genre {
|
||||
__typename: "Genre";
|
||||
name: string;
|
||||
webUrl: string;
|
||||
}
|
||||
|
||||
interface BookGenre {
|
||||
__typename: "BookGenre";
|
||||
genre: Genre;
|
||||
}
|
||||
|
||||
interface Language {
|
||||
__typename: "Language";
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface BookDetails {
|
||||
__typename: "BookDetails";
|
||||
asin: string;
|
||||
format: string;
|
||||
numPages: number;
|
||||
publicationTime: number;
|
||||
publisher: string;
|
||||
isbn: string;
|
||||
isbn13: string;
|
||||
language: Language;
|
||||
}
|
||||
|
||||
interface Book {
|
||||
__typename: "Book";
|
||||
id: string;
|
||||
legacyId: number;
|
||||
webUrl: string;
|
||||
title: string;
|
||||
titleComplete: string;
|
||||
description: string;
|
||||
primaryContributorEdge: BookContributorEdge;
|
||||
secondaryContributorEdges: BookContributorEdge[];
|
||||
imageUrl: string;
|
||||
bookSeries: BookSeries[];
|
||||
bookGenres: BookGenre[];
|
||||
details: BookDetails;
|
||||
work: Ref;
|
||||
}
|
||||
|
||||
interface ContributorWorksConnection {
|
||||
__typename: "ContributorWorksConnection";
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
interface ContributorFollowersConnection {
|
||||
__typename: "ContributorFollowersConnection";
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
interface Contributor {
|
||||
__typename: "Contributor";
|
||||
id: string;
|
||||
legacyId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
isGrAuthor: boolean;
|
||||
works: ContributorWorksConnection;
|
||||
profileImageUrl: string;
|
||||
webUrl: string;
|
||||
followers: ContributorFollowersConnection;
|
||||
}
|
||||
|
||||
interface Series {
|
||||
__typename: "Series";
|
||||
id: string;
|
||||
title: string;
|
||||
webUrl: string;
|
||||
}
|
||||
|
||||
interface Work {
|
||||
__typename: "Work";
|
||||
id: string;
|
||||
legacyId: number;
|
||||
bestBook: Ref;
|
||||
// ...Book
|
||||
}
|
||||
|
||||
interface Query {
|
||||
__typename: "Query";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface NextData {
|
||||
props: {
|
||||
pageProps: {
|
||||
apolloState: {
|
||||
ROOT_QUERY: Query;
|
||||
[key: string]: any;
|
||||
};
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
jwtToken: string;
|
||||
dataSource: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
legacyId: number;
|
||||
title: string;
|
||||
authors: string[];
|
||||
avgRating: number;
|
||||
ratingCount: number;
|
||||
publicationYear: number;
|
||||
editionCount: number;
|
||||
coverImageUrl: string;
|
||||
}
|
||||
|
||||
export function createBookFromNextData(
|
||||
nextData: NextData,
|
||||
ref: Ref
|
||||
): OutputBook {
|
||||
const apolloState = nextData.props.pageProps.apolloState;
|
||||
const bookData = apolloState[ref.__ref] as Book;
|
||||
|
||||
const contributorEdges = [
|
||||
bookData.primaryContributorEdge,
|
||||
...bookData.secondaryContributorEdges,
|
||||
];
|
||||
const authors = contributorEdges
|
||||
.filter((edge) => edge.role === "Author")
|
||||
.map<Contributor>((edge) => apolloState[edge.node.__ref])
|
||||
.map<Author>((contributor) => ({
|
||||
id: contributor.id,
|
||||
legacyId: contributor.legacyId,
|
||||
name: contributor.name,
|
||||
description: contributor.description,
|
||||
}));
|
||||
|
||||
let series: OutputSeries | null = null;
|
||||
if (bookData.bookSeries.length > 0) {
|
||||
const bookSeries = bookData.bookSeries[0];
|
||||
const seriesData = apolloState[bookSeries.series.__ref] as Series;
|
||||
series = {
|
||||
title: seriesData.title,
|
||||
position: parseInt(bookSeries.userPosition, 10),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: bookData.title,
|
||||
description: bookData.description,
|
||||
authors,
|
||||
series,
|
||||
publisher: bookData.details.publisher,
|
||||
publishedAt: new Date(bookData.details.publicationTime),
|
||||
genres: bookData.bookGenres.map((genre) => genre.genre.name),
|
||||
coverImageUrl: bookData.imageUrl,
|
||||
pageCount: bookData.details.numPages,
|
||||
isbn: bookData.details.isbn,
|
||||
isbn13: bookData.details.isbn13,
|
||||
};
|
||||
}
|
||||
|
||||
async function getNextData(legacyId: number): Promise<NextData> {
|
||||
const url = "https://www.goodreads.com/book/show/" + legacyId;
|
||||
const res = await requestUrl({ url });
|
||||
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
||||
let nextDataRaw = doc.getElementById("__NEXT_DATA__")?.textContent;
|
||||
if (typeof nextDataRaw !== "string") {
|
||||
throw new Error("Unable to find next data script in the document.");
|
||||
}
|
||||
const nextData = JSON.parse(nextDataRaw) as NextData;
|
||||
|
||||
return nextData;
|
||||
}
|
||||
|
||||
export async function getBookByLegacyId(legacyId: number): Promise<OutputBook> {
|
||||
const nextData = await getNextData(legacyId);
|
||||
const bookRef = nextData.props.pageProps.apolloState.ROOT_QUERY[
|
||||
`getBookByLegacyId({"legacyId":"${legacyId}"})`
|
||||
] as Ref | undefined;
|
||||
if (bookRef === undefined) {
|
||||
throw new Error("Could not find reference for book.");
|
||||
}
|
||||
|
||||
return createBookFromNextData(nextData, bookRef);
|
||||
}
|
||||
|
||||
export async function searchBooks(q: string): Promise<SearchResult[]> {
|
||||
const url = "https://www.goodreads.com/search?q=" + encodeURIComponent(q);
|
||||
const res = await requestUrl({ url });
|
||||
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
||||
|
||||
return Array.from(
|
||||
doc.querySelectorAll(
|
||||
"table.tableList tr[itemtype='http://schema.org/Book']"
|
||||
)
|
||||
).map<SearchResult>((el) => {
|
||||
const legacyId = parseInt(
|
||||
el.querySelector("div.u-anchorTarget")?.id ?? "",
|
||||
10
|
||||
);
|
||||
|
||||
const title =
|
||||
el.querySelector("a.bookTitle")?.textContent?.trim() || "";
|
||||
|
||||
const authors = Array.from(el.querySelectorAll("a.authorName")).map(
|
||||
(a) => a.textContent?.trim() || ""
|
||||
);
|
||||
|
||||
const avgRating = parseFloat(
|
||||
el
|
||||
.querySelector("span.minirating")
|
||||
?.textContent?.match(/(\d+\.\d+) avg rating/)?.[1] ?? "0"
|
||||
);
|
||||
|
||||
const ratingCount = parseInt(
|
||||
el
|
||||
.querySelector("span.minirating")
|
||||
?.textContent?.match(/(\d[\d,]*) ratings/)?.[1] ?? "0",
|
||||
10
|
||||
);
|
||||
|
||||
const publicationYear = parseInt(
|
||||
el
|
||||
.querySelector("span.greyText")
|
||||
?.textContent?.match(/published (\d{4})/)?.[1] ?? "0",
|
||||
10
|
||||
);
|
||||
|
||||
const editionCount = parseInt(
|
||||
el
|
||||
.querySelector("span.greyText")
|
||||
?.textContent?.match(/(\d+) editions/)?.[1] ?? "0",
|
||||
10
|
||||
);
|
||||
|
||||
const coverImageUrl =
|
||||
el.querySelector("img.bookCover")?.getAttribute("src") || "";
|
||||
|
||||
return {
|
||||
legacyId,
|
||||
title,
|
||||
authors,
|
||||
avgRating,
|
||||
ratingCount,
|
||||
publicationYear,
|
||||
editionCount,
|
||||
coverImageUrl,
|
||||
};
|
||||
});
|
||||
}
|
16
src/main.ts
16
src/main.ts
|
@ -17,12 +17,14 @@ import { LogReadingFinishedCommand } from "@commands/LogReadingFinishedCommand";
|
|||
import { ResetReadingStatusCommand } from "@commands/ResetReadingStatusCommand";
|
||||
import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand";
|
||||
import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand";
|
||||
import { Goodreads } from "@data-sources/Goodreads";
|
||||
|
||||
export default class BookTrackerPlugin extends Plugin {
|
||||
settings: BookTrackerPluginSettings;
|
||||
templater: Templater;
|
||||
storage: Storage;
|
||||
readingLog: ReadingLog;
|
||||
public settings: BookTrackerPluginSettings;
|
||||
public templater: Templater;
|
||||
public storage: Storage;
|
||||
public readingLog: ReadingLog;
|
||||
public goodreads: Goodreads = new Goodreads();
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
@ -32,7 +34,11 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
this.readingLog = new ReadingLog(this.storage);
|
||||
|
||||
this.addCommand(
|
||||
new SearchGoodreadsCommand(this.app, this.createEntry.bind(this))
|
||||
new SearchGoodreadsCommand(
|
||||
this.app,
|
||||
this.goodreads,
|
||||
this.createEntry.bind(this)
|
||||
)
|
||||
);
|
||||
this.addCommand(new LogReadingStartedCommand(this.app, this.settings));
|
||||
this.addCommand(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import GoodreadsSearchModalView from "./GoodreadsSearchModalView.svelte";
|
||||
import { type SearchResult } from "@data-sources/goodreads";
|
||||
import { Goodreads, type SearchResult } from "@data-sources/Goodreads";
|
||||
import { App } from "obsidian";
|
||||
import { SvelteModal } from "./SvelteModal";
|
||||
|
||||
|
@ -8,23 +8,33 @@ export class GoodreadsSearchModal extends SvelteModal<
|
|||
> {
|
||||
constructor(
|
||||
app: App,
|
||||
goodreads: Goodreads,
|
||||
onSearch: (error: any, results: SearchResult[]) => void = () => {}
|
||||
) {
|
||||
super(app, GoodreadsSearchModalView, { props: { onSearch } });
|
||||
super(app, GoodreadsSearchModalView, {
|
||||
props: { goodreads, onSearch },
|
||||
});
|
||||
}
|
||||
|
||||
static createAndOpen(app: App): Promise<SearchResult[]> {
|
||||
static createAndOpen(
|
||||
app: App,
|
||||
goodreads: Goodreads
|
||||
): Promise<SearchResult[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const modal = new GoodreadsSearchModal(app, (error, results) => {
|
||||
modal.close();
|
||||
const modal = new GoodreadsSearchModal(
|
||||
app,
|
||||
goodreads,
|
||||
(error, results) => {
|
||||
modal.close();
|
||||
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(results);
|
||||
}
|
||||
|
||||
resolve(results);
|
||||
});
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { searchBooks, type SearchResult } from "@data-sources/goodreads";
|
||||
import { Goodreads, type SearchResult } from "@data-sources/Goodreads";
|
||||
|
||||
interface Props {
|
||||
goodreads: Goodreads;
|
||||
onSearch: (error: any, results?: SearchResult[]) => void;
|
||||
}
|
||||
|
||||
let { onSearch }: Props = $props();
|
||||
let { goodreads, onSearch }: Props = $props();
|
||||
|
||||
let query = $state("");
|
||||
|
||||
|
@ -13,7 +14,7 @@
|
|||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const results = await searchBooks(query);
|
||||
const results = await goodreads.search(query);
|
||||
if (results.length === 0) {
|
||||
onSearch(new Error("No results found."));
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import GoodreadsSearchSuggestion from "./GoodreadsSearchSuggestion.svelte";
|
||||
import { type SearchResult } from "@data-sources/goodreads";
|
||||
import { App, Notice, SuggestModal } from "obsidian";
|
||||
import { type SearchResult } from "@data-sources/Goodreads";
|
||||
import { App, SuggestModal } from "obsidian";
|
||||
import { mount } from "svelte";
|
||||
|
||||
export class GoodreadsSearchSuggestModal extends SuggestModal<SearchResult> {
|
||||
|
|
|
@ -1,53 +1,59 @@
|
|||
<script lang="ts">
|
||||
import type { SearchResult } from "@data-sources/goodreads";
|
||||
import type { SearchResult } from "@data-sources/Goodreads";
|
||||
|
||||
interface Props {
|
||||
searchResult: SearchResult;
|
||||
}
|
||||
interface Props {
|
||||
searchResult: SearchResult;
|
||||
}
|
||||
|
||||
let { searchResult }: Props = $props();
|
||||
let { searchResult }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="obt-goodreads-search-suggestion">
|
||||
<img class="cover" src={searchResult.coverImageUrl} alt={searchResult.title} />
|
||||
<div class="details">
|
||||
<h3 class="title">{searchResult.title}</h3>
|
||||
<p class="extra-details">
|
||||
<span class="authors">{searchResult.authors.join(", ")}</span>
|
||||
{#if searchResult.publicationYear > 0}
|
||||
<span class="publicationYear">({searchResult.publicationYear})</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
class="cover"
|
||||
src={searchResult.coverImageUrl}
|
||||
alt={searchResult.title}
|
||||
/>
|
||||
<div class="details">
|
||||
<h3 class="title">{searchResult.title}</h3>
|
||||
<p class="extra-details">
|
||||
<span class="authors">{searchResult.authors.join(", ")}</span>
|
||||
{#if searchResult.publicationYear > 0}
|
||||
<span class="publicationYear"
|
||||
>({searchResult.publicationYear})</span
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.obt-goodreads-search-suggestion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.obt-goodreads-search-suggestion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img.cover {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
margin-right: var(--size-4-2);
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
img.cover {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
margin-right: var(--size-4-2);
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
|
||||
.title {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-medium);
|
||||
}
|
||||
.title {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-medium);
|
||||
}
|
||||
|
||||
.extra-details {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.extra-details {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue