new: add search query suggestion,
change: fetch api optimize with api key.
enhance: searchbar will auto add tags from url.
This commit is contained in:
defaultkavy 2024-10-08 02:05:02 +08:00
parent 77d4f78cc2
commit 73894739d0
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
12 changed files with 98 additions and 32 deletions

1
dist/assets/index-BM2d6uNq.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/index.html vendored
View File

@ -7,7 +7,7 @@
<title>Danbooru Viewer v0.2.5</title>
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<script type="module" crossorigin src="/assets/index-Djy_8N6Q.js"></script>
<script type="module" crossorigin src="/assets/index-BM2d6uNq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BW2KEYV0.css">
</head>
<body>

View File

@ -2,7 +2,7 @@
"name": "danbooru-viewer",
"module": "index.ts",
"type": "module",
"version": "0.4.0",
"version": "0.5.0",
"devDependencies": {
"@types/bun": "latest",
"vite": "^5.4.8"

View File

@ -62,6 +62,7 @@ export class $Searchbar extends $Container {
const addTag = () => {e.preventDefault(); this.$tagInput.addTag().input()}
const addSelectedTag = ($selection: $Selection) => {
const inputIndex = this.$tagInput.children.indexOf(this.$tagInput.$inputor);
if (this.$tagInput.$input.value().at(-1) === ':') return this.getSearchSuggestions();
const nextTag = this.$tagInput.children.array.at(inputIndex + 1) as $Tag;
this.$tagInput.addTag($selection.value());
if (nextTag) this.$tagInput.editTag(nextTag);
@ -94,7 +95,7 @@ export class $Searchbar extends $Container {
e.preventDefault();
const inputIndex = this.$tagInput.children.indexOf(this.$tagInput.$inputor)
if (e.shiftKey) {
this.$tagInput.editTag(this.$tagInput.children.array.at(inputIndex - 1) as $Tag)
if (inputIndex - 1 >= 0) this.$tagInput.editTag(this.$tagInput.children.array.at(inputIndex - 1) as $Tag)
break;
}
if (this.$selectionList.focused) addSelectedTag(this.$selectionList.focused);
@ -128,8 +129,7 @@ export class $Searchbar extends $Container {
}
async getSearchSuggestions() {
const input = this.$tagInput.$input.value()
if (!input.length) return this.$selectionList.clearSelections();
const input = this.$tagInput.$input.value();
const results = await Autocomplete.fetch(Booru.used, input, 20);
this.$selectionList
.clearSelections()
@ -148,9 +148,8 @@ export class $Searchbar extends $Container {
]) : null,
data.isUser() ? $('span').class('user-level').content(data.level) : null
])
.on('click', () => {this.$tagInput.addTag(data.label).input()})
.on('click', () => {this.$tagInput.addTag(data.value).input()})
))
if (!this.$tagInput.$input.value().length) this.$selectionList.clearSelections();
}
search() {
@ -163,6 +162,10 @@ export class $Searchbar extends $Container {
checkURL(beforeURL: URL | undefined, afterURL: URL) {
if (beforeURL?.hash === '#search') this.inactivate();
if (afterURL.hash === '#search') this.activate();
if (`${beforeURL?.pathname}${beforeURL?.search}` === `${afterURL.pathname}${afterURL.search}`) return;
const tags_string = afterURL.searchParams.get('tags');
this.$tagInput.clearAll();
tags_string?.split(' ').forEach(tag => this.$tagInput.addTag(tag));
}
}
@ -269,8 +272,8 @@ class $TagInput extends $Container {
input() {
this.insert(this.$inputor);
this.$input.focus();
if (this.$input.value()) this.$seachbar.getSearchSuggestions();
else this.$seachbar.$selectionList.clearSelections();
this.$seachbar.$selectionList.clearSelections();
this.$seachbar.getSearchSuggestions();
return this;
}
@ -281,7 +284,8 @@ class $TagInput extends $Container {
$tag.on('click', () => this.editTag($tag))
this.tags.add($tag);
this.value('');
this.$inputor.replace($tag);
if (this.$input.inDOM()) this.$inputor.replace($tag);
else this.insert($tag);
return this;
}
@ -309,6 +313,11 @@ class $TagInput extends $Container {
return this;
}
focus() {
this.$input.focus();
return this;
}
get query() { return this.tags.array.map(tag => tag.name).toString().replace(',', '+') }
}

View File

@ -1,12 +1,70 @@
import type { Booru } from "./Booru";
import type { TagCategory } from "./Tag";
import type { UserLevelString } from "./User";
import type { UserLevel } from "./User";
export class Autocomplete {
static async fetch(booru: Booru, query: string, limit: number = 20) {
const res = await fetch(`${booru.origin}/autocomplete.json?search[query]=${query}&search[type]=tag_query&version=1&limit=${limit}`).then(res => res.json()) as AutocompleteData[];
return res.map(data => new AutocompleteResult(data));
if (!query.length) return this.searchQuery.map(data => new AutocompleteResult(data))
const res = await booru.fetch<AutocompleteData[]>(`/autocomplete.json?search[query]=${query}&search[type]=tag_query&version=1&limit=${limit}`);
const searchQueryResult = query.length ? this.searchQuery.filter(sq => sq.value.startsWith(query) && sq.value !== query) : this.searchQuery
return [...searchQueryResult, ...res].map(data => new AutocompleteResult(data));
}
static searchQuery: AutocompleteSearchQueryData[] = [
{value: 'user:', label: 'user:'},
{value: 'approver:', label: 'approver:'},
{value: '-approver:', label: '-approver:'},
{value: 'order:', label: 'order:'},
{value: 'ordfav:', label: 'ordfav:'},
{value: 'ordfavgroup:', label: 'ordfavgroup:'},
{value: 'search:', label: 'search:'},
{value: 'favgroup:', label: 'favgroup:'},
{value: '-favgroup:', label: '-favgroup:'},
{value: 'favcount:', label: 'favcount:'},
{value: 'id:', label: 'id:'},
{value: 'tagcount:', label: 'tagcount:'},
{value: 'gentags:', label: 'gentags:'},
{value: 'arttags:', label: 'arttags:'},
{value: 'chartags:', label: 'chartags:'},
{value: 'copytags:', label: 'copytags:'},
{value: 'metatags:', label: 'metatags:'},
{value: 'score:', label: 'score:'},
{value: 'upvote:', label: 'upvote:'},
{value: 'downvote:', label: 'downvote:'},
{value: 'disapproved:', label: 'disapproved:'},
{value: 'md5:', label: 'md5:'},
{value: 'width:', label: 'width:'},
{value: 'height:', label: 'height:'},
{value: 'ratio:', label: 'ratio:'},
{value: 'mpixels:', label: 'mpixels:'},
{value: 'filesize:', label: 'filesize:'},
{value: 'duration:', label: 'duration:'},
{value: 'is:', label: 'is:'},
{value: 'has:', label: 'has:'},
{value: 'pool:', label: 'pool:'},
{value: '-pool:', label: '-pool:'},
{value: 'ordpool:', label: 'ordpool:'},
{value: 'random:', label: 'random:'},
{value: 'limit:', label: 'limit:'},
{value: 'date:', label: 'date:'},
{value: 'commenter:', label: 'commenter:'},
{value: 'note:', label: 'note:'},
{value: 'noter:', label: 'noter:'},
{value: 'noteupdater:', label: 'noteupdater:'},
{value: 'status:', label: 'status:'},
{value: '-status:', label: '-status:'},
{value: 'rating:', label: 'rating:'},
{value: '-rating:', label: '-rating:'},
{value: 'source:', label: 'source:'},
{value: '-source:', label: '-source:'},
{value: 'pixiv:', label: 'pixiv:'},
{value: 'parent:', label: 'parent:'},
{value: 'child:', label: 'child:'},
{value: 'flagger:', label: 'flagger:'},
{value: 'appealer:', label: 'appealer:'},
{value: 'commentary:', label: 'commentary:'},
{value: 'commentaryupdater:', label: 'commentaryupdater:'},
].map(data => ({type: 'query', ...data}))
}
export interface AutocompleteResult extends AutocompleteBaseData {}
@ -37,10 +95,10 @@ export class AutocompleteResult {
}
}
type AutocompleteData = AutocompleteBaseData & (AutocompleteUserData | AutocompleteTagData | AutocompleteTagAutocorrectData | AutocompleteTagAliasData);
type AutocompleteData = AutocompleteBaseData & (AutocompleteUserData | AutocompleteTagData | AutocompleteTagAutocorrectData | AutocompleteTagAliasData | AutocompleteSearchQueryData);
interface AutocompleteBaseData {
type: 'user' | 'tag' | 'tag-autocorrect' | 'tag-alias' | 'tag-word';
type: 'user' | 'tag' | 'tag-autocorrect' | 'tag-alias' | 'tag-word' | 'query';
label: string;
value: string;
}
@ -48,7 +106,7 @@ interface AutocompleteBaseData {
interface AutocompleteUserData {
type: 'user';
id: number;
level: Lowercase<UserLevelString>;
level: Lowercase<keyof UserLevel>;
}
interface AutocompleteTagData {
type: 'tag';
@ -73,3 +131,5 @@ interface AutocompleteTagWordData{
post_count: number;
antecedent: string;
}
interface AutocompleteSearchQueryData {type: 'query', value: string, label: string}

View File

@ -36,7 +36,8 @@ export class Booru {
static set storageAPI(name: string | null) { if (name) localStorage.setItem('booru_api', name); else localStorage.removeItem('booru_api') }
async fetch<T>(endpoint: string) {
const data = await fetch(`${this.origin}${endpoint}`).then(res => res.json()) as any;
const auth = this.user ? `${endpoint.includes('?') ? '&' : '?'}login=${this.user.name}&api_key=${this.user.apiKey}` : '';
const data = await fetch(`${this.origin}${endpoint}${auth}`).then(res => res.json()) as any;
if (data.success === false) throw data.message;
return data as T;
}

View File

@ -8,8 +8,8 @@ export class ArtistCommentary {
}
static async fetch(booru: Booru, id: id) {
const req = await fetch(`${booru.origin}/artist_commentaries/${id}.json`);
const post = new this(await req.json());
const data = await booru.fetch<ArtistCommentaryData>(`/artist_commentaries/${id}.json`);
const post = new this(data);
return post;
}
@ -26,8 +26,7 @@ export class ArtistCommentary {
else searchQuery += `&search[${key}]=${val}`
}
}
const req = await fetch(`${booru.origin}/artist_commentaries.json?limit=${limit}${searchQuery}`);
const dataArray: ArtistCommentaryData[] = await req.json();
const dataArray = await booru.fetch<ArtistCommentaryData[]>(`/artist_commentaries.json?limit=${limit}${searchQuery}`);
const list = dataArray.map(data => {
const instance = new this(data);
this.manager.set(instance.id, instance);

View File

@ -39,7 +39,7 @@ export class Post extends $EventManager<{update: []}> {
}
async fetch() {
const data = await fetch(`${this.booru.origin}/posts/${this.id}.json`).then(async data => await data.json()) as PostData;
const data = await this.booru.fetch<PostData>(`/posts/${this.id}.json`);
this.update(data);
User.fetchMultiple(this.booru, {id: [this.uploader_id, this.approver_id].detype(null)}).then(() => this.update$());
return this;
@ -58,8 +58,7 @@ export class Post extends $EventManager<{update: []}> {
}
}
}
const req = await fetch(`${booru.origin}/posts.json?limit=${limit}&tags=${tagsQuery}&_method=get`);
const dataArray: PostData[] = await req.json();
const dataArray = await booru.fetch<PostData[]>(`/posts.json?limit=${limit}&tags=${tagsQuery}&_method=get`);
if (dataArray instanceof Array === false) return [];
const list = dataArray.map(data => {
const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id, data);

View File

@ -13,7 +13,7 @@ export class Tag {
}
static async fetch(booru: Booru, id: id) {
const data = await fetch(`${booru.origin}/tags/${id}.json`).then(async data => await data.json()) as TagData;
const data = await booru.fetch<TagData>(`/tags/${id}.json`);
const instance = booru.tags.get(data.id)?.update(data) ?? new this(booru, data);
booru.tags.set(instance.id, instance);
return instance;
@ -32,8 +32,7 @@ export class Tag {
else searchQuery += `&search[${key}]=${val}`
}
}
const req = await fetch(`${booru.origin}/tags.json?limit=${limit}${searchQuery}`);
const dataArray: TagData[] = await req.json();
const dataArray = await booru.fetch<TagData[]>(`/tags.json?limit=${limit}${searchQuery}`);
const list = dataArray.map(data => {
const instance = booru.tags.get(data.id)?.update(data) ?? new this(booru, data);
booru.tags.set(instance.id, instance);

View File

View File

@ -16,7 +16,7 @@ export class User {
}
static async fetch(booru: Booru, id: id) {
const data = await fetch(`${booru.origin}/users/${id}.json`).then(async data => await data.json()) as UserData;
const data = await booru.fetch<UserData>(`/users/${id}.json`);
const instance = this.manager.get(data.id)?.update(data) ?? new this(booru, data);
this.manager.set(instance.id, instance);
return instance;
@ -35,8 +35,7 @@ export class User {
else searchQuery += `&search[${key}]=${val}`
}
}
const req = await fetch(`${booru.origin}/users.json?limit=${limit}${searchQuery}`);
const dataArray: UserData[] = await req.json();
const dataArray = await booru.fetch<UserData[]>(`/users.json?limit=${limit}${searchQuery}`);
const list = dataArray.map(data => {
const instance = new this(booru, data);
this.manager.set(instance.id, instance);