initial. s/book/game/g. model RAWG Api games response and request. remove service providers and other unneeded things for this fork

This commit is contained in:
calvin 2023-03-21 15:11:48 -04:00
parent 0dc93919fd
commit c3732cdbaf
28 changed files with 223 additions and 1057 deletions

View file

@ -1,35 +0,0 @@
# 옵시디언 책 검색 플러그인
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/anpigon/obsidian-book-search-plugin/Release%20Obsidian%20plugin?logo=github)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/anpigon/obsidian-book-search-plugin?sort=semver)
[![Korean](https://img.shields.io/badge/Language-English-blueviolet)](README.md)
옵시디언에서 독서노트를 간단하게 생성할 수 있습니다.
<br>
## 데모 영상
https://user-images.githubusercontent.com/3969643/184918274-8ad24546-2e01-4288-a855-c8eeb1feca7d.mp4
<br>
## 사용 방법
_작성 중..._
<br>
## 블로그 글
- [[옵시디언] '네이버 책 검색 API'를 사용하여 독서노트 생성하기](https://anpigon.tistory.com/177)
- [[옵시디언] 책 검색 플러그인 (Book Search Plugin v0.1.0)](https://anpigon.tistory.com/165)
<br>
## 개발 지원
이 플러그인 개발에 도움을 주고 싶다면, 저에게 커피 한 잔을 사주세요.
<a href="https://www.buymeacoffee.com/anpigon" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="60"></a>&nbsp;
<a href="https://anpigon.github.io/buymeacoffee/"><img src="https://user-images.githubusercontent.com/3969643/184924261-f0224843-08fa-4bce-af70-dc5db589979f.png" height="60"></a>

217
README.md
View file

@ -1,240 +1,45 @@
# Obsidian Book Search Plugin
# Obsidian Game Search Plugin
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/anpigon/obsidian-book-search-plugin/Release%20Obsidian%20plugin?logo=github)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/anpigon/obsidian-book-search-plugin?sort=semver)
[![Korean](https://img.shields.io/badge/Language-한국어-blueviolet)](README.ko.md)
Easily create book notes.
<br>
## Changelog
### [0.5.8](https://github.com/anpigon/obsidian-book-search-plugin/compare/0.5.8-beta.2...0.5.8) (2022-09-13)
### Features
- Add locale selection for Google searches.
- Can use the [Templater plugin](https://github.com/SilentVoid13/Templater) with.
- Enables [Inline scripts](https://github.com/anpigon/obsidian-book-search-plugin#inline-script) for templates.
<br>
## Demo
https://user-images.githubusercontent.com/3969643/184918274-8ad24546-2e01-4288-a855-c8eeb1feca7d.mp4
<br>
## forked from
https://github.com/anpigon/obsidian-book-search-plugin/
## Description
Use to query book using :
Use to query game using :
- A book title, author, publisher or ISBN (10 or 13).
- A game title
Use Google Books API to get the book information.
Use RAWG Game APe to get the game information.
<br>
## How to install
Click the link to install the Book Search plugin: [Install Link](https://obsidian.md/plugins?id=obsidian-book-search-plugin)
Click the link to install the Game Search plugin: [Install Link](https://github.com/CMorooney/obsidian-game-search-plugin)
Or, Search in the Obsidian Community plugin. And install it.
<img width="700" src="https://user-images.githubusercontent.com/3969643/184918934-585375a9-7b25-4905-81c8-5f092ed74991.png">
<br>
## How to use
### 1. Click the ribbon icon, or excute the command "Create new book note".
### 1. Click the ribbon icon, or excute the command "Create new game note".
<img width="600" src="https://user-images.githubusercontent.com/3969643/161973483-ab007598-e0b8-433f-9697-75ee0ef74195.png">
### 2. Search for game by keywords.
### 2. Search for books by keywords.
<img width="600" src="https://user-images.githubusercontent.com/3969643/161973979-51f642c9-626a-4015-a7e9-dfdbe6ec2cbc.png">
### 3. Select the book from the search results.
<img width="600" src="https://user-images.githubusercontent.com/3969643/161974310-13c3b39b-51dc-472f-b787-db64f74caf74.png">
### 3. Select the game from the search results.
### 4. Voila! A note has been created.
<img width="600" src="https://user-images.githubusercontent.com/3969643/161974593-1b7bfe69-cb9d-47d7-a43d-1d725295a122.png">
<br>
## How to use settings
<img width="700" src="https://user-images.githubusercontent.com/3969643/184919550-68eff0e4-2b02-41bb-8f17-30a5354359a3.png">
### New file location
Set the folder location where the new file is created. Otherwise, a new file is created in the Obsidian Root folder.
### New file name
You can set the file name format. The default format is `{{title}} - {{author}}`.
You can set the file name format. The default format is `{{name}} - {{published}}`.
You can use `{{DATE}}` or `{{DATE:YYYYMMDD}}` to set a unique file name.
### Template file
You can set the template file location. There is an example template at the bottom.
### Service Provider
You can set up the services that you use to search for books. Only Google and Naver(네이버) are available now.
To use Naver Book Search, clientId and clientSecret are required. I will explain how to get clientId and clientSecret from Naver on my blog.
### <strike>(Deprecated) Text to insert into front matter</strike>
<strike>You can add the following to the default Front Matter, or create a new Front Matter with the structure you want.</strike> Please use the template file described below.
### <strike>(Deprecated) Text to insert into content</strike>
<strike>You can add text to the content for [Dataview inline metadata](https://blacksmithgu.github.io/obsidian-dataview/data-annotation/#pages).</strike> Please use the template file described below.
<br>
## Example template
Please also find a definition of the variables used in this template below (see: [Template variables definitions](#template-variables-definitions)).
```
---
tag: 📚Book
title: "{{title}}"
author: [{{author}}]
publisher: {{publisher}}
publish: {{publishDate}}
total: {{totalPage}}
isbn: {{isbn10}} {{isbn13}}
cover: {{coverUrl}}
status: unread
created: {{DATE:YYYY-MM-DD HH:mm:ss}}
updated: {{DATE:YYYY-MM-DD HH:mm:ss}}
---
![cover|150]({{coverUrl}})
# {{title}}
```
<br>
## Dataview rendering
<img width="1024" alt="" src="https://user-images.githubusercontent.com/3969643/184546096-82ccaae6-9893-411b-aed6-a72c54f72cb2.png">
Here is the dataview query used in the demo
````
# 📚 My Bookshelf
```dataview
TABLE WITHOUT ID
status as Status,
rows.file.link as Book
FROM #📚Book
WHERE !contains(file.path, "Templates")
GROUP BY status
SORT status
```
## List of all books
```dataview
TABLE WITHOUT ID
status as Status,
"![|60](" + cover + ")" as Cover,
link(file.link, title) as Title,
author as Author,
join(list(publisher, publish)) as Publisher
FROM #📚Book
WHERE !contains(file.path, "Templates")
SORT status DESC, file.ctime ASC
```
````
The banner at the top of the document is rendered using [Obsidian-banners](https://github.com/noatpad/obsidian-banners) plugin.
<br>
## Template variables definitions
Please find here a definition of the possible variables to be used in your template. Simply write `{{name}}` in your template, and replace name by the desired book data, including:
| name | description |
| ----------- | ------------------------------------------------------- |
| title | The title of the book. |
| author | The name of the book author. It can be multiple people. |
| category | Book category. |
| description | Book description. |
| publisher | The publisher of the book. |
| totalPage | The total number of pages in the book. |
| coverUrl | Book cover image URL. |
| publishDate | The year the book was published. |
| isbn10 | ISBN10 |
| isbn13 | ISBN13 |
<br>
## Advanced
### Inline Script
#### To print out a book object:
````
```json
<%=book%>
```
````
or
````
```json
<%=JSON.stringify(book, null, 2)%>
```
````
#### When you want to list or link authors:
```
---
authors: <%=book.authors.map(author=>`\n - ${author}`).join('')%>
---
authors: <%=book.authors.map(author => `[[${author}]]`).join(', ')%>
```
<br>
## License
[Obsidian Book Search Plugin](https://github.com/anpigon/obsidian-book-search-plugin) is licensed under the GNU AGPLv3 license. Refer to [LICENSE](https://github.com/SilentVoid13/Templater/blob/master/LICENSE.TXT) for more information.
<br>
## Contributing
Feel free to contribute.
You can create an [issue](https://github.com/anpigon/obsidian-book-search-plugin/issues) to report a bug, suggest an improvement for this plugin, ask a question, etc.
You can make a [pull request](https://github.com/anpigon/obsidian-book-search-plugin/pulls) to contribute to this plugin development.
<br>
## Support
If this plugin helped you and you wish to contribute :)
Buy me coffee on [buymeacoffee.com/anpigon](https://www.buymeacoffee.com/anpigon)
<a href="https://www.buymeacoffee.com/anpigon" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="60"></a>&nbsp;
<a href="https://anpigon.github.io/buymeacoffee/"><img src="https://user-images.githubusercontent.com/3969643/184924261-f0224843-08fa-4bce-af70-dc5db589979f.png" height="60"></a>

View file

@ -1,10 +1,10 @@
{
"id": "obsidian-book-search-plugin",
"name": "Book Search",
"version": "0.5.8",
"id": "obsidian-game-search-plugin",
"name": "Game Search",
"version": "0.1.0",
"minAppVersion": "0.15.0",
"description": "Helps you find books and create notes.",
"author": "anpigon",
"authorUrl": "https://github.com/anpigon",
"description": "Helps you find Games and create notes.",
"author": "Calving forked from anpigon",
"authorUrl": "https://github.com/CMorooney/obsidian-game-search-plugin",
"isDesktopOnly": false
}

View file

@ -1,10 +1,10 @@
{
"id": "obsidian-book-search-plugin",
"name": "Book Search",
"version": "0.5.9",
"id": "obsidian-game-search-plugin",
"name": "Game Search",
"version": "0.1.0",
"minAppVersion": "0.15.0",
"description": "Helps you find books and create notes.",
"author": "anpigon",
"authorUrl": "https://github.com/anpigon",
"description": "Helps you find games and create notes.",
"author": "Calvin forked from anpigon",
"authorUrl": "https://github.com/CMorooney/obsidian-game-search-plugin",
"isDesktopOnly": false
}

View file

@ -1,19 +1,20 @@
{
"name": "obsidian-book-search-plugin",
"version": "0.5.9",
"description": "This is a plugin to help you create book notes.",
"homepage": "https://anpigon.github.io/obsidian-book-search-plugin/",
"name": "obsidian-game-search-plugin",
"version": "0.1.0",
"description": "This is a plugin to help you create game notes.",
"homepage": "https://github.com/CMorooney/obsidian-game-search-plugin",
"main": "main.js",
"standard-version": {
"t": ""
},
"keywords": [
"book",
"book note",
"game",
"game note",
"video games",
"obsidian",
"plugin"
],
"author": "anpigon",
"author": "Calvin forked from anpigon",
"license": "MIT",
"engines": {
"node": ">= 16",

View file

@ -5,11 +5,11 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Obsidian Book Search Plugin</title>
<title>Obsidian Game Search Plugin</title>
</head>
<body>
<h1>Obsidian Book Search Plugin</h1>
<h1>Obsidian Game Search Plugin</h1>
</body>
</html>
</html>

View file

@ -2,8 +2,8 @@
<html lang="en">
<head>
<meta http-equiv="refresh" content="0; url=obsidian://show-plugin?id=obsidian-book-search-plugin">
<title>Install Obsidian Book Search Plugin</title>
<meta http-equiv="refresh" content="0; url=obsidian://show-plugin?id=obsidian-game-search-plugin">
<title>Install Obsidian Game Search Plugin</title>
</head>
</html>
</html>

View file

@ -1,44 +0,0 @@
import { Book } from '@models/book.model';
import { BookSearchPluginSettings } from '@settings/settings';
import { ServiceProvider } from '@src/constants';
import { requestUrl } from 'obsidian';
import { GoogleBooksApi } from './google_books_api';
import { NaverBooksApi } from './naver_books_api';
export interface BaseBooksApiImpl {
getByQuery(query: string): Promise<Book[]>;
}
export function factoryServiceProvider(settings: BookSearchPluginSettings): BaseBooksApiImpl {
if (settings.serviceProvider === ServiceProvider.google) {
return new GoogleBooksApi(settings.localePreference);
}
if (settings.serviceProvider === ServiceProvider.naver) {
if (!settings.naverClientId || !settings.naverClientSecret) {
// TODO: Let's create a custom error class
throw new Error('네이버 개발자센터에서 "Client ID"와 "Client Secret"를 발급받아 설정해주세요.');
}
return new NaverBooksApi(settings.naverClientId, settings.naverClientSecret);
}
}
export async function apiGet<T>(
url: string,
params: Record<string, string | number> = {},
headers?: Record<string, string>,
): Promise<T> {
const apiURL = new URL(url);
Object.entries(params).forEach(([key, value]) => {
apiURL.searchParams.append(key, value?.toString());
});
const res = await requestUrl({
url: apiURL.href,
method: 'GET',
headers: {
Accept: '*/*',
'Content-Type': 'application/json; charset=utf-8',
...headers,
},
});
return res.json as T;
}

View file

@ -1,110 +0,0 @@
import { Book } from '@models/book.model';
import { GoogleBooksApi } from './google_books_api';
import { Type, VolumeInfo } from './models/google_books_response';
describe('Book creation', () => {
const volumeInfo: VolumeInfo = {
title: 'Flow',
subtitle: 'The Psychology of Optimal Experience',
authors: ['Mihaly Csikszentmihalyi'],
publisher: 'Harper Collins',
publishedDate: '2009-10-13',
description:
'\u003cp\u003e“Csikszentmihalyi arrives at an insight that many of us can intuitively grasp, despite our insistent (and culturally supported) denial of this truth. That is, it is not what happens to us that determines our happiness, but the manner in which we make sense of that reality. . . . The manner in which Csikszentmihalyi integrates research on consciousness, personal psychology and spirituality is illuminating.” —Los Angeles Times Book Review\u003c/p\u003e\u003cp\u003eThe bestselling classic that holds the key to unlocking meaning, creativity, peak performance, and true happiness. \u003c/p\u003e\u003cp\u003eLegendary psychologist Mihaly Csikszentmihalyi\'s famous investigations of "optimal experience" have revealed that what makes an experience genuinely satisfying is a state of consciousness called flow. During flow, people typically experience deep enjoyment, creativity, and a total involvement with life. In this new edition of his groundbreaking classic work, Csikszentmihalyi ("the leading researcher into flow states" —Newsweek) demonstrates the ways this positive state can be controlled, not just left to chance. Flow: The Psychology of Optimal Experience teaches how, by ordering the information that enters our consciousness, we can discover true happiness, unlock our potential, and greatly improve the quality of our lives.\u003c/p\u003e',
industryIdentifiers: [
{
type: Type.Isbn10,
identifier: '0061876720',
},
{
type: Type.Isbn13,
identifier: '9780061876721',
},
],
readingModes: {
text: true,
image: false,
},
pageCount: 336,
printType: 'BOOK',
categories: ['Psychology / Creative Ability', 'Psychology / Applied Psychology', 'Psychology / Personality'],
averageRating: 4,
ratingsCount: 1404,
maturityRating: 'NOT_MATURE',
allowAnonLogging: true,
contentVersion: '1.4.3.0.preview.2',
panelizationSummary: {
containsEpubBubbles: false,
containsImageBubbles: false,
},
imageLinks: {
smallThumbnail:
'http://books.google.com/books/content?id=QVjPsd1UukEC&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE71MowoDIZtesyLc6kiXVlnvQznYNgPj7fIAedD1BNhyuPtLC5i3QuDwZKM_5Q-FZXaf0tMfBx2ijWUBEcVqPmYkK6ApHUMJVxsIlEP2maBsJJHIU2_De5ioR5KVAF0za48f39aA&source=gbs_api',
thumbnail:
'http://books.google.com/books/content?id=QVjPsd1UukEC&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE71SvX9ABv9Ensxle-fchWlnS8UEYiU3Symajbi_0pbwhrZFP3ahRLKcK_RjWJP9pRMtbjSSK3SDbr_UXWQ8GbCnncMVCs37gYWGedBePMEbk4G9eCsiiWy22ejJIJ3H3bx53Huq&source=gbs_api',
},
language: 'en',
previewLink: 'http://books.google.fr/books?id=QVjPsd1UukEC&hl=&source=gbs_api',
infoLink: 'https://play.google.com/store/books/details?id=QVjPsd1UukEC&source=gbs_api',
canonicalVolumeLink: 'https://play.google.com/store/books/details?id=QVjPsd1UukEC',
};
const api: GoogleBooksApi = new GoogleBooksApi('default');
const book: Book = api.createBookItem(volumeInfo);
it('Title', () => {
expect(book.title).toEqual(volumeInfo.title);
});
it('Subtitle', () => {
expect(book.subtitle).toEqual(volumeInfo.subtitle);
});
it('Author', () => {
expect(book.author).toEqual(api.formatList(volumeInfo.authors));
});
it('Category', () => {
expect(book.category).toEqual(api.formatList(volumeInfo.categories));
});
it('Publisher', () => {
expect(book.publisher).toEqual(volumeInfo.publisher);
});
it('Published date', () => {
expect(book.publishDate).toEqual(volumeInfo.publishedDate);
});
it('Total pages', () => {
expect(book.totalPage).toEqual(volumeInfo.pageCount);
});
it('Cover URL', () => {
expect(book.coverUrl).toEqual(volumeInfo.imageLinks.thumbnail);
});
it('Cover small URL', () => {
expect(book.coverSmallUrl).toEqual(volumeInfo.imageLinks.smallThumbnail);
});
it('Description', () => {
expect(book.description).toEqual(volumeInfo.description);
});
it('Link', () => {
expect(book.link).toEqual(volumeInfo.canonicalVolumeLink);
});
it('Preview link', () => {
expect(book.previewLink).toEqual(volumeInfo.previewLink);
});
it('ISBN 10', () => {
expect(book.isbn10).toEqual(volumeInfo.industryIdentifiers[0].identifier);
});
it('ISBN 13', () => {
expect(book.isbn13).toEqual(volumeInfo.industryIdentifiers[1].identifier);
});
});

View file

@ -1,74 +0,0 @@
import { Book } from '@models/book.model';
import { apiGet, BaseBooksApiImpl } from '@apis/base_api';
import { GoogleBooksResponse, VolumeInfo } from './models/google_books_response';
export class GoogleBooksApi implements BaseBooksApiImpl {
constructor(private readonly localePreference: string) {}
async getByQuery(query: string) {
try {
const params = {
q: query,
maxResults: 40,
printType: 'books',
};
const langRestrict = this.localePreference;
if (langRestrict === 'default') {
params['langRestrict'] = window.moment.locale();
} else {
params['langRestrict'] = langRestrict;
}
const searchResults = await apiGet<GoogleBooksResponse>('https://www.googleapis.com/books/v1/volumes', params);
if (!searchResults?.totalItems) {
return [];
}
return searchResults.items.map(({ volumeInfo }) => this.createBookItem(volumeInfo));
} catch (error) {
console.warn(error);
throw error;
}
}
getISBN(industryIdentifiers: VolumeInfo['industryIdentifiers']) {
return industryIdentifiers?.reduce((result, item) => {
if (item.type == 'ISBN_10') {
result['isbn10'] = item.identifier.trim();
}
if (item.type == 'ISBN_13') {
result['isbn13'] = item.identifier.trim();
}
return result;
}, {});
}
createBookItem(item: VolumeInfo): Book {
const book: Book = {
title: item.title,
subtitle: item.subtitle,
author: this.formatList(item.authors),
authors: item.authors,
category: this.formatList(item.categories),
publisher: item.publisher,
totalPage: item.pageCount,
coverUrl: item.imageLinks?.thumbnail ?? '',
coverSmallUrl: item.imageLinks?.smallThumbnail ?? '',
publishDate: item.publishedDate || '',
description: item.description,
link: item.canonicalVolumeLink || item.infoLink,
previewLink: item.previewLink,
...this.getISBN(item.industryIdentifiers),
};
return book;
}
convertGoogleBookImageURLSize(url: string, zoom: number) {
return url.replace(/(&zoom)=\d/, `$1=${zoom}`);
}
formatList(list?: string[]) {
if (list?.length > 1) {
return list.map(item => `${item.trim()}`).join(', ');
}
return list?.[0]?.replace('N/A', '') ?? '';
}
}

View file

@ -1,122 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface GoogleBooksResponse {
kind: string;
totalItems: number;
items: Item[];
}
export interface Item {
kind: string;
id: string;
etag: string;
selfLink: string;
volumeInfo: VolumeInfo;
saleInfo: SaleInfo;
accessInfo: AccessInfo;
searchInfo: SearchInfo;
}
export interface AccessInfo {
country: string;
viewability: string;
embeddable: boolean;
publicDomain: boolean;
textToSpeechPermission: string;
epub: Epub;
pdf: Epub;
webReaderLink: string;
accessViewStatus: string;
quoteSharingAllowed: boolean;
}
export interface Epub {
isAvailable: boolean;
acsTokenLink?: string;
}
export interface SaleInfo {
country: string;
saleability: string;
isEbook: boolean;
listPrice?: SaleInfoListPrice;
retailPrice?: SaleInfoListPrice;
buyLink?: string;
offers?: Offer[];
}
export interface SaleInfoListPrice {
amount: number;
currencyCode: string;
}
export interface Offer {
finskyOfferType: number;
listPrice: OfferListPrice;
retailPrice: OfferListPrice;
rentalDuration?: RentalDuration;
}
export interface OfferListPrice {
amountInMicros: number;
currencyCode: string;
}
export interface RentalDuration {
unit: string;
count: number;
}
export interface SearchInfo {
textSnippet: string;
}
export interface VolumeInfo {
title: string;
authors?: string[];
publisher?: string;
publishedDate: string;
industryIdentifiers: IndustryIdentifier[];
readingModes: ReadingModes;
pageCount?: number;
printType: string;
categories?: string[];
maturityRating: string;
allowAnonLogging: boolean;
contentVersion: string;
panelizationSummary: PanelizationSummary;
imageLinks: ImageLinks;
language: string;
previewLink: string;
infoLink: string;
canonicalVolumeLink: string;
subtitle?: string;
description?: string;
averageRating?: number;
ratingsCount?: number;
}
export interface ImageLinks {
smallThumbnail: string;
thumbnail: string;
}
export interface IndustryIdentifier {
type: Type;
identifier: string;
}
export enum Type {
Isbn10 = 'ISBN_10',
Isbn13 = 'ISBN_13',
Other = 'OTHER',
}
export interface PanelizationSummary {
containsEpubBubbles: boolean;
containsImageBubbles: boolean;
}
export interface ReadingModes {
text: boolean;
image: boolean;
}

View file

@ -1,19 +0,0 @@
export interface NaverBooksResponse {
lastBuildDate: string;
total: number;
start: number;
display: number;
items: NaverBookItem[];
}
export interface NaverBookItem {
title: string;
link: string;
image: string;
author: string;
discount: string;
publisher: string;
pubdate: string;
isbn: string;
description: string;
}

View file

@ -1,47 +0,0 @@
import { Book } from '@models/book.model';
import { apiGet, BaseBooksApiImpl } from './base_api';
import { NaverBookItem, NaverBooksResponse } from './models/naver_books_response';
export class NaverBooksApi implements BaseBooksApiImpl {
constructor(private readonly clientId, private readonly clientSecret: string) {}
async getByQuery(query: string) {
try {
const params = {
query,
display: 50,
sort: 'sim',
};
const header = {
'X-Naver-Client-Id': this.clientId,
'X-Naver-Client-Secret': this.clientSecret,
};
const searchResults = await apiGet<NaverBooksResponse>(
'https://openapi.naver.com/v1/search/book.json',
params,
header,
);
if (!searchResults?.total) {
return [];
}
return searchResults.items.map(this.createBookItem);
} catch (error) {
console.warn(error);
throw error;
}
}
createBookItem(item: NaverBookItem) {
return {
title: item.title,
author: item.author,
publisher: item.publisher,
coverUrl: item.image,
publishDate: item.pubdate?.slice(0, 4) || '',
link: item.link,
description: item.description,
isbn: item.isbn,
...(item.isbn?.length >= 13 ? { isbn13: item.isbn } : { isbn10: item.isbn }),
} as Book;
}
}

View file

@ -0,0 +1,30 @@
import { RAWGResponse, Game } from '@models/game.model';
import { requestUrl } from 'obsidian';
export class RAWGAPI {
constructor(private readonly key: string) {}
async getByQuery(query: string): Promise<Game[]> {
try {
const apiURL = new URL('https://api.rawg.io/api/games');
apiURL.searchParams.append('key', this.key);
apiURL.searchParams.append('search', query);
const res = await requestUrl({
url: apiURL.href,
method: 'GET',
});
const searchResults = res.json as RAWGResponse<Game>;
if (!searchResults?.count) {
return [];
}
return searchResults.results;
} catch (error) {
console.warn(error);
throw error;
}
}
}

View file

@ -1,4 +0,0 @@
export enum ServiceProvider {
google = 'google',
naver = 'naver',
}

View file

@ -1,10 +1,10 @@
import { MarkdownView, Notice, Plugin } from 'obsidian';
import { BookSearchModal } from '@views/book_search_modal';
import { BookSuggestModal } from '@views/book_suggest_modal';
import { GameSearchModal } from '@views/game_search_modal';
import { GameSuggestModal } from '@views/game_suggest_modal';
import { CursorJumper } from '@utils/cursor_jumper';
import { Book } from '@models/book.model';
import { BookSearchSettingTab, BookSearchPluginSettings, DEFAULT_SETTINGS } from '@settings/settings';
import { Game } from '@models/game.model';
import { GameSearchSettingTab, GameSearchPluginSettings, DEFAULT_SETTINGS } from '@settings/settings';
import {
getTemplateContents,
applyTemplateTransformations,
@ -13,34 +13,34 @@ import {
} from '@utils/template';
import { replaceVariableSyntax, makeFileName, applyDefaultFrontMatter, toStringFrontMatter } from '@utils/utils';
export default class BookSearchPlugin extends Plugin {
settings: BookSearchPluginSettings;
export default class GameSearchPlugin extends Plugin {
settings: GameSearchPluginSettings;
async onload() {
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('book', 'Create new book note', () => this.createNewBookNote());
const ribbonIconEl = this.addRibbonIcon('game', 'Create new game note', () => this.createNewGameNote());
// Perform additional things with the ribbon
ribbonIconEl.addClass('obsidian-book-search-plugin-ribbon-class');
ribbonIconEl.addClass('obsidian-game-search-plugin-ribbon-class');
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-book-search-modal',
name: 'Create new book note',
callback: () => this.createNewBookNote(),
id: 'open-game-search-modal',
name: 'Create new game note',
callback: () => this.createNewGameNote(),
});
this.addCommand({
id: 'open-book-search-modal-to-insert',
id: 'open-game-search-modal-to-insert',
name: 'Insert the metadata',
callback: () => this.insertMetadata(),
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new BookSearchSettingTab(this.app, this));
this.addSettingTab(new GameSearchSettingTab(this.app, this));
console.log(`Book Search: version ${this.manifest.version} (requires obsidian ${this.manifest.minAppVersion})`);
console.log(`Game Search: version ${this.manifest.version} (requires obsidian ${this.manifest.minAppVersion})`);
}
showNotice(message: unknown) {
@ -51,13 +51,13 @@ export default class BookSearchPlugin extends Plugin {
}
}
// open modal for book search
async searchBookMetadata(query?: string): Promise<Book> {
const searchedBooks = await this.openBookSearchModal(query);
return await this.openBookSuggestModal(searchedBooks);
// open modal for game search
async searchGameMetadata(query?: string): Promise<Game> {
const searchedGames = await this.openGameSearchModal(query);
return await this.openGameSuggestModal(searchedGames);
}
async getRenderedContents(book: Book) {
async getRenderedContents(game: Game) {
const {
templateFile,
useDefaultFrontmatter,
@ -68,17 +68,17 @@ export default class BookSearchPlugin extends Plugin {
if (templateFile) {
const templateContents = await getTemplateContents(this.app, templateFile);
const replacedVariable = replaceVariableSyntax(book, applyTemplateTransformations(templateContents));
return executeInlineScriptsTemplates(book, replacedVariable);
const replacedVariable = replaceVariableSyntax(game, applyTemplateTransformations(templateContents));
return executeInlineScriptsTemplates(game, replacedVariable);
}
let replacedVariableFrontmatter = replaceVariableSyntax(book, frontmatter); // @deprecated
let replacedVariableFrontmatter = replaceVariableSyntax(game, frontmatter); // @deprecated
if (useDefaultFrontmatter) {
replacedVariableFrontmatter = toStringFrontMatter(
applyDefaultFrontMatter(book, replacedVariableFrontmatter, defaultFrontmatterKeyType),
applyDefaultFrontMatter(game, replacedVariableFrontmatter, defaultFrontmatterKeyType),
);
}
const replacedVariableContent = replaceVariableSyntax(book, content);
const replacedVariableContent = replaceVariableSyntax(game, content);
return replacedVariableFrontmatter
? `---\n${replacedVariableFrontmatter}\n---\n${replacedVariableContent}`
@ -94,14 +94,14 @@ export default class BookSearchPlugin extends Plugin {
}
// TODO: Try using a search query on the selected text
const book = await this.searchBookMetadata(markdownView.file.basename);
const game = await this.searchGameMetadata(markdownView.file.basename);
if (!markdownView.editor) {
console.warn('Can not find editor from the active markdown view');
return;
}
const renderedContents = await this.getRenderedContents(book);
const renderedContents = await this.getRenderedContents(game);
markdownView.editor.replaceRange(renderedContents, { line: 0, ch: 0 });
} catch (err) {
console.warn(err);
@ -109,9 +109,9 @@ export default class BookSearchPlugin extends Plugin {
}
}
async createNewBookNote(): Promise<void> {
async createNewGameNote(): Promise<void> {
try {
const book = await this.searchBookMetadata();
const game = await this.searchGameMetadata();
// open file
const activeLeaf = this.app.workspace.getLeaf();
@ -120,11 +120,11 @@ export default class BookSearchPlugin extends Plugin {
return;
}
const renderedContents = await this.getRenderedContents(book);
const renderedContents = await this.getRenderedContents(game);
// TODO: If the same file exists, it asks if you want to overwrite it.
// create new File
const fileName = makeFileName(book, this.settings.fileNameFormat);
const fileName = makeFileName(game, this.settings.fileNameFormat);
const filePath = `${this.settings.folder}/${fileName}`;
const targetFile = await this.app.vault.create(filePath, renderedContents);
@ -143,18 +143,18 @@ export default class BookSearchPlugin extends Plugin {
}
}
async openBookSearchModal(query = ''): Promise<Book[]> {
async openGameSearchModal(query = ''): Promise<Game[]> {
return new Promise((resolve, reject) => {
return new BookSearchModal(this, query, (error, results) => {
return new GameSearchModal(this, this.settings.rawgApiKey, query, (error, results) => {
return error ? reject(error) : resolve(results);
}).open();
});
}
async openBookSuggestModal(books: Book[]): Promise<Book> {
async openGameSuggestModal(games: Game[]): Promise<Game> {
return new Promise((resolve, reject) => {
return new BookSuggestModal(this.app, books, (error, selectedBook) => {
return error ? reject(error) : resolve(selectedBook);
return new GameSuggestModal(this.app, games, (error, selectedGame) => {
return error ? reject(error) : resolve(selectedGame);
}).open();
});
}

View file

@ -1,29 +0,0 @@
export interface FrontMatter {
[key: string]: string | string[];
}
export interface Book {
title: string; // 책 제목
subtitle?: string;
author: string; // 저자
authors: string[];
category?: string; // 카테고리
publisher?: string; // 출판사
publishDate?: string; // 출판일
totalPage?: number | string; // 전체 페이지
coverUrl?: string; // 커버 URL
coverSmallUrl?: string; // 커버 URL
coverMediumUrl?: string; // 커버 URL
coverLargeUrl?: string; // 커버 URL
status?: string; // 읽기 상태(읽기전, 읽는중, 읽기완료)
startReadDate?: string; // 읽기 시작한 일시
finishReadDate?: string; // 읽기 완료한 일시
myRate?: number | string; //나의 평점
bookNote?: string; //서평 기록 여부
isbn10?: string;
isbn13?: string;
isbn?: string;
link?: string;
description?: string;
previewLink?: string;
}

34
src/models/game.model.ts Normal file
View file

@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface FrontMatter {
[key: string]: string | string[];
}
export interface RAWGResponse<T> {
count: number;
next: string;
previous: string;
results: T[];
}
export interface Game {
id: number;
slug: string;
name: string;
released: string;
tba: boolean;
background_image: string;
rating: number;
rating_top: number;
ratings: any;
ratings_count: number;
reviews_text_count: number;
added: number;
added_by_status: number;
metacritic: number;
playtime: number; // in hours
suggestions_count: number;
updated: string;
esrb_rating?: string;
platforms: any[];
}

View file

@ -1,21 +1,19 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import { replaceDateInString } from '@utils/utils';
import BookSearchPlugin from '../main';
import GameSearchPlugin from '../main';
import { FileNameFormatSuggest } from './suggesters/FileNameFormatSuggester';
import { FolderSuggest } from './suggesters/FolderSuggester';
import { FileSuggest } from './suggesters/FileSuggester';
import { ServiceProvider } from '@src/constants';
import { SettingServiceProviderModal } from '@views/setting_service_provider_modal';
const docUrl = 'https://github.com/anpigon/obsidian-book-search-plugin';
const docUrl = 'https://github.com/CMorooney/obsidian-game-search-plugin';
export enum DefaultFrontmatterKeyType {
snakeCase = 'Snake Case',
camelCase = 'Camel Case',
}
export interface BookSearchPluginSettings {
export interface GameSearchPluginSettings {
folder: string; // new file location
fileNameFormat: string; // new file name format
frontmatter: string; // frontmatter that is inserted into the file
@ -23,13 +21,10 @@ export interface BookSearchPluginSettings {
useDefaultFrontmatter: boolean;
defaultFrontmatterKeyType: DefaultFrontmatterKeyType;
templateFile: string;
serviceProvider: ServiceProvider;
naverClientId: string;
naverClientSecret: string;
localePreference: string;
rawgApiKey: string;
}
export const DEFAULT_SETTINGS: BookSearchPluginSettings = {
export const DEFAULT_SETTINGS: GameSearchPluginSettings = {
folder: '',
fileNameFormat: '',
frontmatter: '',
@ -37,14 +32,11 @@ export const DEFAULT_SETTINGS: BookSearchPluginSettings = {
useDefaultFrontmatter: true,
defaultFrontmatterKeyType: DefaultFrontmatterKeyType.camelCase,
templateFile: '',
serviceProvider: ServiceProvider.google,
naverClientId: '',
naverClientSecret: '',
localePreference: 'default',
rawgApiKey: '',
};
export class BookSearchSettingTab extends PluginSettingTab {
constructor(app: App, private plugin: BookSearchPlugin) {
export class GameSearchSettingTab extends PluginSettingTab {
constructor(app: App, private plugin: GameSearchPlugin) {
super(app, plugin);
}
@ -57,14 +49,14 @@ export class BookSearchSettingTab extends PluginSettingTab {
containerEl.empty();
containerEl.classList.add('book-search-plugin__settings');
containerEl.classList.add('game-search-plugin__settings');
createHeader(containerEl, 'General Settings');
// New file location
new Setting(containerEl)
.setName('New file location')
.setDesc('New book notes will be placed here.')
.setDesc('New game notes will be placed here.')
.addSearch(cb => {
try {
new FolderSuggest(this.app, cb.inputEl);
@ -84,7 +76,7 @@ export class BookSearchSettingTab extends PluginSettingTab {
text: replaceDateInString(this.plugin.settings.fileNameFormat) || '{{title}} - {{author}}',
});
new Setting(containerEl)
.setClass('book-search-plugin__settings--new_file_name')
.setClass('game-search-plugin__settings--new_file_name')
.setName('New file name')
.setDesc('Enter the file name format.')
.addSearch(cb => {
@ -93,18 +85,18 @@ export class BookSearchSettingTab extends PluginSettingTab {
} catch {
// eslint-disable
}
cb.setPlaceholder('Example: {{title}} - {{author}}')
cb.setPlaceholder('Example: {{name}} - {{release}}')
.setValue(this.plugin.settings.fileNameFormat)
.onChange(newValue => {
this.plugin.settings.fileNameFormat = newValue?.trim();
this.plugin.saveSettings();
newFileNameHint.innerHTML = replaceDateInString(newValue) || '{{title}} - {{author}}';
newFileNameHint.innerHTML = replaceDateInString(newValue) || '{{name}} - {{release}}';
});
});
containerEl
.createEl('div', {
cls: ['setting-item-description', 'book-search-plugin__settings--new_file_name_hint'],
cls: ['setting-item-description', 'game-search-plugin__settings--new_file_name_hint'],
})
.append(newFileNameHint);
@ -132,86 +124,12 @@ export class BookSearchSettingTab extends PluginSettingTab {
});
});
// Service Provider
let serviceProviderExtraSettingButton: HTMLElement;
// eslint-disable-next-line prefer-const
let preferredLocaleDropdownSetting: Setting;
const hideServiceProviderExtraSettingButton = () => {
serviceProviderExtraSettingButton.addClass('book-search-plugin__hide');
};
const showServiceProviderExtraSettingButton = () => {
serviceProviderExtraSettingButton.removeClass('book-search-plugin__hide');
};
const hideServiceProviderExtraSettingDropdown = () => {
if (preferredLocaleDropdownSetting !== undefined) {
preferredLocaleDropdownSetting.settingEl.addClass('book-search-plugin__hide');
}
};
const showServiceProviderExtraSettingDropdown = () => {
if (preferredLocaleDropdownSetting !== undefined) {
preferredLocaleDropdownSetting.settingEl.removeClass('book-search-plugin__hide');
}
};
const toggleServiceProviderExtraSettings = (serviceProvider: ServiceProvider = this.settings?.serviceProvider) => {
if (serviceProvider === ServiceProvider.naver) {
showServiceProviderExtraSettingButton();
hideServiceProviderExtraSettingDropdown();
} else {
hideServiceProviderExtraSettingButton();
showServiceProviderExtraSettingDropdown();
}
};
new Setting(containerEl)
.setName('Service Provider')
.setDesc('Choose the service provider you want to use to search your books.')
.setClass('book-search-plugin__settings--service_provider')
.addDropdown(dropDown => {
dropDown.addOption(ServiceProvider.google, `${ServiceProvider.google} (Global)`);
dropDown.addOption(ServiceProvider.naver, `${ServiceProvider.naver} (Korean)`);
dropDown.setValue(this.plugin.settings?.serviceProvider ?? ServiceProvider.google);
dropDown.onChange(async value => {
const newValue = value as ServiceProvider;
toggleServiceProviderExtraSettings(newValue);
this.settings['serviceProvider'] = newValue;
await this.plugin.saveSettings();
});
})
.addExtraButton(component => {
serviceProviderExtraSettingButton = component.extraSettingsEl;
toggleServiceProviderExtraSettings();
component.onClick(() => {
new SettingServiceProviderModal(this.plugin).open();
});
});
preferredLocaleDropdownSetting = new Setting(containerEl)
.setName('Preferred locale')
.setDesc('Sets the preferred locale to use when searching for books.')
.addDropdown(dropDown => {
const defaultLocale = window.moment.locale();
dropDown.addOption(defaultLocale, `${defaultLocale} (Default Locale)`);
window.moment.locales().forEach(locale => {
dropDown.addOption(locale, locale);
});
const setValue = this.settings.localePreference;
if (setValue === 'default') {
dropDown.setValue(defaultLocale);
} else {
dropDown.setValue(setValue);
}
dropDown.onChange(async value => {
const newValue = value;
this.settings.localePreference = newValue;
await this.plugin.saveSettings();
});
});
// Frontmatter Settings
const formatterSettingsChildren: Setting[] = [];
createFoldingHeader(containerEl, 'Frontmatter Settings', formatterSettingsChildren);
formatterSettingsChildren.push(
new Setting(containerEl)
.setClass('book-search-plugin__hide')
.setClass('game-search-plugin__hide')
.setName('Use the default frontmatter')
.setDesc("If you don't want the default frontmatter to be inserted, disable it.")
.addToggle(toggle => {
@ -222,7 +140,7 @@ export class BookSearchSettingTab extends PluginSettingTab {
});
}),
new Setting(containerEl)
.setClass('book-search-plugin__hide')
.setClass('game-search-plugin__hide')
.setName('Default frontmatter key type')
.setDesc(createKeyTypeDesc())
.addDropdown(dropDown => {
@ -235,7 +153,7 @@ export class BookSearchSettingTab extends PluginSettingTab {
});
}),
new Setting(containerEl)
.setClass('book-search-plugin__hide')
.setClass('game-search-plugin__hide')
.setName('(Deprecated) Text to insert into frontmatter')
.setDesc(createSyntaxesDescription('#text-to-insert-into-frontmatter'))
.addTextArea(textArea => {
@ -253,7 +171,7 @@ export class BookSearchSettingTab extends PluginSettingTab {
createFoldingHeader(containerEl, 'Content Settings', contentSettingsChildren);
contentSettingsChildren.push(
new Setting(containerEl)
.setClass('book-search-plugin__hide')
.setClass('game-search-plugin__hide')
.setName('(Deprecated) Text to insert into content')
.setDesc(createSyntaxesDescription('#text-to-insert-into-content'))
.addTextArea(textArea => {
@ -290,7 +208,7 @@ function createFoldingHeader(containerEl: HTMLElement, title: string, formatterS
return createHeader(containerEl, title).addToggle(toggle => {
toggle.onChange(checked => {
formatterSettingsChildren.forEach(({ settingEl }) => {
settingEl.toggleClass('book-search-plugin__show', checked);
settingEl.toggleClass('game-search-plugin__show', checked);
});
});
});

View file

@ -1,4 +1,4 @@
import { Book } from '@models/book.model';
import { Game } from '@models/game.model';
import { App, normalizePath, Notice, TFile } from 'obsidian';
export async function getTemplateContents(app: App, templatePath: string | undefined): Promise<string> {
@ -43,7 +43,7 @@ export function applyTemplateTransformations(rawTemplateContents: string): strin
);
}
export function executeInlineScriptsTemplates(book: Book, text: string) {
export function executeInlineScriptsTemplates(game: Game, text: string) {
const commandRegex = /<%(?:=)(.+)%>/g;
const ctor = getFunctionConstructor();
const matchedList = [...text.matchAll(commandRegex)];
@ -51,12 +51,12 @@ export function executeInlineScriptsTemplates(book: Book, text: string) {
try {
const outputs = new ctor(
[
'const [book] = arguments',
'const [game] = arguments',
`const output = ${script}`,
'if(typeof output === "string") return output',
'return JSON.stringify(output)',
].join(';'),
)(book);
)(game);
return result.replace(matched, outputs);
} catch (err) {
console.warn(err);

View file

@ -1,46 +0,0 @@
import { Book } from '@models/book.model';
import * as utils from './utils';
jest.mock('@settings/settings', () => jest.fn());
describe('util.js', () => {
const book: Book = {
title: '코스모스',
author: '칼 세이건',
authors: ['칼 세이건'],
};
it('replaceIllegalFileNameCharactersInString', () => {
expect(utils.replaceIllegalFileNameCharactersInString('재레드 다이아몬드의 <대변동 : 위기, 선택, 변화>')).toBe(
'재레드 다이아몬드의 대변동 위기 선택 변화',
);
});
it('makeFileName 1', () => {
expect(utils.makeFileName(book)).toBe('코스모스 - 칼 세이건.md');
});
it('makeFileName 2', () => {
const newBook = {
...book,
author: '',
};
expect(utils.makeFileName(newBook)).toBe('코스모스.md');
});
it('makeFileName 3', () => {
expect(utils.makeFileName(book, '{{author}}-{{title}}')).toBe('칼 세이건-코스모스.md');
});
it('makeFileName 4', () => {
expect(utils.makeFileName(book, '{{author}}-{{title}}')).toBe('칼 세이건-코스모스.md');
});
it('makeFileName 5', () => {
const newBook = {
...book,
title: '코스모스 : 창백한 푸른점',
};
expect(utils.makeFileName(newBook, '{{title}} - {{author}}')).toBe('코스모스 창백한 푸른점 - 칼 세이건.md');
});
});

View file

@ -1,4 +1,4 @@
import { Book, FrontMatter } from '@models/book.model';
import { Game, FrontMatter } from '@models/game.model';
import { DefaultFrontmatterKeyType } from '@settings/settings';
// == Format Syntax == //
@ -14,29 +14,29 @@ export function isISBN(str: string) {
return /^(97(8|9))?\d{9}(\d|X)$/.test(str);
}
export function makeFileName(book: Book, fileNameFormat?: string) {
export function makeFileName(game: Game, fileNameFormat?: string) {
let result;
if (fileNameFormat) {
result = replaceVariableSyntax(book, replaceDateInString(fileNameFormat));
result = replaceVariableSyntax(game, replaceDateInString(fileNameFormat));
} else {
result = !book.author ? book.title : `${book.title} - ${book.author}`;
result = !game.released ? game.name : `${game.name} - ${game.released}`;
}
return replaceIllegalFileNameCharactersInString(result) + '.md';
}
export function changeSnakeCase(book: Book) {
return Object.entries(book).reduce((acc, [key, value]) => {
export function changeSnakeCase(game: Game) {
return Object.entries(game).reduce((acc, [key, value]) => {
acc[camelToSnakeCase(key)] = value;
return acc;
}, {});
}
export function applyDefaultFrontMatter(
book: Book,
game: Game,
frontmatter: FrontMatter | string,
keyType: DefaultFrontmatterKeyType = DefaultFrontmatterKeyType.snakeCase,
) {
const frontMater = keyType === DefaultFrontmatterKeyType.camelCase ? book : changeSnakeCase(book);
const frontMater = keyType === DefaultFrontmatterKeyType.camelCase ? game : changeSnakeCase(game);
const extraFrontMatter = typeof frontmatter === 'string' ? parseFrontMatter(frontmatter) : frontmatter;
for (const key in extraFrontMatter) {
@ -51,12 +51,12 @@ export function applyDefaultFrontMatter(
return frontMater as object;
}
export function replaceVariableSyntax(book: Book, text: string): string {
export function replaceVariableSyntax(game: Game, text: string): string {
if (!text?.trim()) {
return '';
}
const entries = Object.entries(book);
const entries = Object.entries(game);
return entries
.reduce((result, [key, val = '']) => {

View file

@ -1,40 +0,0 @@
import { App, SuggestModal } from 'obsidian';
import { Book } from '@models/book.model';
export class BookSuggestModal extends SuggestModal<Book> {
constructor(
app: App,
private readonly suggestion: Book[],
private onChoose: (error: Error | null, result?: Book) => void,
) {
super(app);
}
// Returns all available suggestions.
getSuggestions(query: string): Book[] {
return this.suggestion.filter(book => {
const searchQuery = query?.toLowerCase();
return (
book.title?.toLowerCase().includes(searchQuery) ||
book.author?.toLowerCase().includes(searchQuery) ||
book.publisher?.toLowerCase().includes(searchQuery)
);
});
}
// Renders each suggestion item.
renderSuggestion(book: Book, el: HTMLElement) {
const title = book.title;
const publisher = book.publisher ? `, ${book.publisher}` : '';
const publishDate = book.publishDate ? `(${book.publishDate})` : '';
const totalPage = book.totalPage ? `, p${book.totalPage}` : '';
const subtitle = `${book.author}${publisher}${publishDate}${totalPage}`;
el.createEl('div', { text: title });
el.createEl('small', { text: subtitle });
}
// Perform action on the selected suggestion.
onChooseSuggestion(book: Book) {
this.onChoose(null, book);
}
}

View file

@ -1,20 +1,19 @@
import { ButtonComponent, Modal, Setting, TextComponent, Notice } from 'obsidian';
import { Book } from '@models/book.model';
import { BaseBooksApiImpl, factoryServiceProvider } from '@apis/base_api';
import BookSearchPlugin from '@src/main';
import { RAWGAPI } from '@src/apis/rawg_games_api';
import { Game } from '@models/game.model';
import GameSearchPlugin from '@src/main';
export class BookSearchModal extends Modal {
export class GameSearchModal extends Modal {
private isBusy = false;
private okBtnRef?: ButtonComponent;
private serviceProvider: BaseBooksApiImpl;
constructor(
plugin: BookSearchPlugin,
plugin: GameSearchPlugin,
private key: string,
private query: string,
private callback: (error: Error | null, result?: Book[]) => void,
private callback: (error: Error | null, result?: Game[]) => void,
) {
super(plugin.app);
this.serviceProvider = factoryServiceProvider(plugin.settings);
}
setBusy(busy: boolean) {
@ -23,7 +22,7 @@ export class BookSearchModal extends Modal {
this.okBtnRef?.setButtonText(busy ? 'Requesting...' : 'Search');
}
async searchBook() {
async searchGame() {
if (!this.query) {
throw new Error('No query entered.');
}
@ -31,11 +30,12 @@ export class BookSearchModal extends Modal {
if (!this.isBusy) {
try {
this.setBusy(true);
const searchResults = await this.serviceProvider.getByQuery(this.query);
const api = new RAWGAPI(this.key);
const searchResults = await api.getByQuery(this.query);
this.setBusy(false);
if (!searchResults?.length) {
new Notice(`No results found for "${this.query}"`); // Couldn't find the book.
new Notice(`No results found for "${this.query}"`); // Couldn't find the game.
return;
}
@ -49,19 +49,19 @@ export class BookSearchModal extends Modal {
submitEnterCallback(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.isComposing) {
this.searchBook();
this.searchGame();
}
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Search Book' });
contentEl.createEl('h2', { text: 'Search Game' });
contentEl.createDiv({ cls: 'book-search-plugin__search-modal--input' }, settingItem => {
contentEl.createDiv({ cls: 'game-search-plugin__search-modal--input' }, settingItem => {
new TextComponent(settingItem)
.setValue(this.query)
.setPlaceholder('Search by keyword or ISBN')
.setPlaceholder('Search by title')
.onChange(value => (this.query = value))
.inputEl.addEventListener('keydown', this.submitEnterCallback.bind(this));
});
@ -71,7 +71,7 @@ export class BookSearchModal extends Modal {
.setButtonText('Search')
.setCta()
.onClick(() => {
this.searchBook();
this.searchGame();
}));
});
}

View file

@ -0,0 +1,30 @@
import { App, SuggestModal } from 'obsidian';
import { Game } from '@models/game.model';
export class GameSuggestModal extends SuggestModal<Game> {
constructor(
app: App,
private readonly suggestion: Game[],
private onChoose: (error: Error | null, result?: Game) => void,
) {
super(app);
}
// Returns all available suggestions.
getSuggestions(_query: string): Game[] {
return this.suggestion;
}
// Renders each suggestion item.
renderSuggestion(game: Game, el: HTMLElement) {
const title = game.name;
const publishDate = game.released ? `(${game.released})` : '';
el.createEl('div', { text: title });
el.createEl('small', { text: publishDate });
}
// Perform action on the selected suggestion.
onChooseSuggestion(game: Game) {
this.onChoose(null, game);
}
}

View file

@ -1,77 +0,0 @@
import { ServiceProvider } from '@src/constants';
import BookSearchPlugin from '@src/main';
import { Modal, Setting } from 'obsidian';
export class SettingServiceProviderModal extends Modal {
private readonly plugin: BookSearchPlugin;
private readonly currentServiceProvider: ServiceProvider;
constructor(plugin: BookSearchPlugin, private callback?: () => void) {
super(plugin.app);
this.plugin = plugin;
this.currentServiceProvider = plugin.settings?.serviceProvider ?? ServiceProvider.google;
}
get settings() {
return this.plugin.settings;
}
async saveSetting() {
return this.plugin.saveSettings();
}
saveClientId(clientId: string) {
if (this.currentServiceProvider === ServiceProvider.naver) {
this.plugin.settings['naverClientId'] = clientId;
}
}
saveClientSecret(clientSecret: string) {
if (this.currentServiceProvider === ServiceProvider.naver) {
this.settings['naverClientSecret'] = clientSecret;
}
}
get currentClientId() {
if (this.currentServiceProvider === ServiceProvider.naver) {
return this.settings.naverClientId;
}
return '';
}
get currentClientSecret() {
if (this.currentServiceProvider === ServiceProvider.naver) {
return this.settings.naverClientSecret;
}
return '';
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Service Provider Setting' });
new Setting(contentEl).setName('Client ID').addText(text => {
text.setValue(this.currentClientId).onChange(value => this.saveClientId(value));
});
new Setting(contentEl).setName('Client Secret').addText(text => {
text.setValue(this.currentClientSecret).onChange(value => this.saveClientSecret(value));
});
new Setting(contentEl).addButton(btn =>
btn
.setButtonText('Save')
.setCta()
.onClick(async () => {
await this.plugin.saveSettings();
this.close();
this.callback?.();
}),
);
}
onClose() {
this.contentEl.empty();
}
}

View file

@ -1,29 +1,29 @@
.book-search-plugin__settings .search-input-container {
game-search-plugin__settings .search-input-container {
width: 100%;
}
.book-search-plugin__settings--new_file_name {
.game-search-plugin__settings--new_file_name {
padding-bottom: 0;
margin-bottom: 0;
}
.book-search-plugin__settings--new_file_name_hint {
.game-search-plugin__settings--new_file_name_hint {
margin-bottom: 20px;
}
.book-search-plugin__settings--service_provider .dropdown {
.game-search-plugin__settings--service_provider .dropdown {
text-transform: capitalize;
}
.book-search-plugin__hide {
.game-search-plugin__hide {
display: none;
}
.book-search-plugin__show {
.game-search-plugin__show {
display: flex;
}
.book-search-plugin__search-modal--input {
.game-search-plugin__search-modal--input {
padding-bottom: 18px;
}
.book-search-plugin__search-modal--input input {
.game-search-plugin__search-modal--input input {
width: 100%;
}

View file

@ -1,5 +0,0 @@
import * as obsidian from 'obsidian';
export const requestUrl: typeof obsidian.requestUrl = req => {
return fetch(req as never).then(res => res.json());
};