change - depart PostManager methods from $PostGrid.
new - navigation posts on post page.
optimize - fetch fewer tags when tags is cached.
change - Post.file_url and Post.file_url$ can be undefined.
change: $PostTile need $PostGrid as params when construct.
change: post page viewer $img will load preview image first.
change: post page become non-static page.
This commit is contained in:
defaultkavy 2024-10-21 09:12:38 +08:00
parent eb2cbf688a
commit dc176f4e83
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
15 changed files with 284 additions and 164 deletions

File diff suppressed because one or more lines are too long

1
dist/assets/index-DAA98Qle.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

4
dist/index.html vendored
View File

@ -16,8 +16,8 @@
gtag('config', 'G-59HBGP98WR');
</script>
<script type="module" crossorigin src="/assets/index-D9u7pURH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BgVVzy-z.css">
<script type="module" crossorigin src="/assets/index-DAA98Qle.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Plu04C2a.css">
</head>
<body>
</body>

View File

@ -2,7 +2,7 @@
"name": "danbooru-viewer",
"module": "index.ts",
"type": "module",
"version": "0.10.5",
"version": "0.11.0",
"scripts": {
"dev": "bun x vite",
"build": "bun x vite build",

View File

@ -0,0 +1,23 @@
import { $Container, $Element, $PointerManager } from "elexis";
import { $View } from "../../../elexis-ext/view";
export class $SlideViewer extends $Container {
pointers = new $PointerManager(this);
$container = $('div')
constructor() {
super('slide-viewer')
this.__build__();
}
protected __build__() {
this.content([ this.$container ]);
this.pointers.on('move', $pointer => {
const [x, y] = [$pointer.move_x, $pointer.move_y];
this.$container.css({transform: `translate(${x}, ${y})`});
})
}
addSlide(id: string, $element: $Element) {
return this;
}
}

View File

@ -37,10 +37,10 @@ export class $DetailPanel extends $Container {
new $Property('favorites').name('Favorites').content(this.post.favcount$),
new $Property('score').name('Score').content(this.post.score$)
]),
new $Property('file-url').name('File').content([
$('a').href(this.post.file_url$).content(this.post.file_url$.convert((value) => value.replace('https://', ''))).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.file_url))
]),
this.post.file_url ? new $Property('file-url').name('File').content([
$('a').href(this.post.file_url$).content(this.post.file_url$.convert((value) => value ? value.replace('https://', '') : '' )).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.file_url!))
]) : null,
new $Property('source-url').name('Source').content([
$('a').href(this.post.source$).content(this.post.source$.convert((value) => value.replace('https://', ''))).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.source))
@ -56,7 +56,7 @@ export class $DetailPanel extends $Container {
]),
$('div').class('post-tags').content(async $tags => {
if (this.options.tagsType === 'detail') {
const tags = await this.post!.fetchTags();
const tags = (await this.post!.fetchTags()).tags;
const [artist_tags, char_tags, gen_tags, meta_tags, copy_tags] = [
tags.filter(tag => tag.category === TagCategory.Artist),
tags.filter(tag => tag.category === TagCategory.Character),

View File

@ -3,33 +3,32 @@ import { Booru } from "../../structure/Booru";
import { Post } from "../../structure/Post";
import { $PostTile } from "../PostTile/$PostTile";
import { $Input } from "elexis/lib/node/$Input";
import { PostManager } from "../../structure/PostManager";
interface $PostGridOptions {
tags?: string
}
export class $PostGrid extends $Layout<$PostGridEventMap> {
posts = new Set<Post>();
export class $PostGrid extends $Layout {
$posts = new Map<Post, $PostTile>();
orderMap = new Map<id, Post>();
tags?: string;
finished = false;
limit = 100;
$focus = $.focus();
posts: PostManager;
constructor(options?: $PostGridOptions) {
super();
this.tags = options?.tags;
this.posts = PostManager.get(this.tags);
this.addStaticClass('post-grid');
this.type('waterfall').gap(10);
this.init();
}
protected async init() {
setInterval(() => { if (this.inDOM() && document.documentElement.scrollTop === 0) this.getPost('newer'); }, 10000);
this.posts.events.on('post_fetch', (posts) => { this.addPost(posts) })
setInterval(async () => { if (this.inDOM() && document.documentElement.scrollTop === 0) await this.posts.fetchPosts('newer'); }, 10000);
Booru.events.on('set', () => {
this.removeAll();
if (this.finished) {
this.finished = false;
this.events.fire('startLoad');
if (this.posts.finished) {
this.posts.finished = false;
this.loader();
}
})
@ -37,7 +36,6 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
// this.on('afterRender', () => {
// this.$focus.currentLayer?.focus(this.$focus.currentLayer.currentFocus);
// })
this.events.fire('startLoad');
this.loader();
this.$focus.layer(100).loop(false).scrollThreshold($.rem(2) + 60);
@ -59,7 +57,7 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
.keydown([' ', 'Enter'], e => {
e.preventDefault();
const focused = this.$focus.currentLayer?.currentFocus;
if (focused instanceof $PostTile) $.open(`/posts/${focused.post.id}`);
if (focused instanceof $PostTile) $.open(focused.url);
})
.keydown(['Escape'], e => { e.preventDefault(); this.$focus.blur(); })
}
@ -67,11 +65,11 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
protected async loader() {
if (!this.inDOM()) return setTimeout(() => this.loader(), 100);;
while (this.inDOM() && document.documentElement.scrollHeight <= innerHeight * 2) {
const posts = await this.getPost('older');
const posts = await this.posts.fetchPosts('older');
if (!posts.length) return;
}
if (document.documentElement.scrollTop + innerHeight > document.documentElement.scrollHeight - innerHeight * 2) {
const posts = await this.getPost('older');
const posts = await this.posts.fetchPosts('older');
if (!posts.length) return;
}
setTimeout(() => this.loader(), 100);
@ -86,13 +84,13 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
posts = $.orArrayResolve(posts);
for (const post of posts) {
if (!post.file_url) continue;
if (this.posts.has(post)) continue;
const $post = new $PostTile(post).on('$focus', (e, $post) => this.$focus.layer(100).focus($post));
if (this.posts.cache.has(post)) continue;
const $post = new $PostTile(this, post).on('$focus', (e, $post) => this.$focus.layer(100).focus($post));
this.$posts.set(post, $post);
this.posts.add(post);
this.posts.cache.add(post);
}
this.$focus.layer(100).elementSet.clear();
const $posts = [...this.orderMap.values()].map(post => {
const $posts = [...this.posts.orderMap.values()].map(post => {
return this.$posts.get(post)?.self(this.$focus.layer(100).add)
});
this.content($posts).render();
@ -102,85 +100,9 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
removeAll() {
this.posts.clear();
this.$posts.clear();
this.orderMap.clear();
this.$focus.layer(100).removeAll();
this.animate({opacity: [1, 0]}, {duration: 300, easing: 'ease'}, () => this.clear().render())
return this;
}
async getPost(direction: 'newer' | 'older'): Promise<Post[]> {
const tags = this.tags ? decodeURIComponent(this.tags).split('+') : undefined;
const generalTags: string[] = [];
const orderTags: string[] = [];
let limit: number = this.limit;
if (tags) for (const tag of tags) {
if (tag.startsWith('ordfav:')) orderTags.push(tag);
else if (tag.startsWith('order:')) orderTags.push(tag);
else if (tag.startsWith('limit:')) limit = Number(tag.split(':')[1]);
else generalTags.push(tag);
}
if (orderTags.length) {
if (orderTags.length > 1) {
this.events.fire('post_error', `Error: These query can't be used together [${orderTags}].`)
return [];
}
const orderTag = orderTags[0];
if (orderTag.startsWith('ordfav:')) {
const username = orderTag.split(':')[1];
const match_tags = generalTags.length ? `&search[post_tags_match]=${generalTags.toString().replaceAll(',', '+')}` : '';
const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `&search[id]=>${this.orderKeyList.at(0)}` : `&search[id]=<${this.orderKeyList.at(-1)}` : undefined;
const favoritesDataList = await Booru.used.fetch<FavoritesData[]>(`/favorites.json?search[user_name]=${username}${beforeAfter ?? ''}${match_tags}&limit=${limit}`);
const posts = await Post.fetchMultiple(Booru.used, {tags: `id:${favoritesDataList.map(data => data.post_id).toString()}`});
const newPostOrderMap = new Map();
for (const fav of favoritesDataList) {
const post = posts.find(post => post.id === fav.post_id);
if (!post) continue;
newPostOrderMap.set(fav.id, post);
}
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap]);
this.addPost(posts);
return posts;
}
if (orderTag.startsWith('order:')) {
const page = this.orderKeyList.length ? direction === 'newer' ? 1 : (this.orderMap.size / limit) + 1 : undefined;
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags}, limit, page);
const newPostOrderMap = new Map(posts.map(post => [post.id, post]));
newPostOrderMap.forEach((post, id) => { if (this.orderMap.has(id)) newPostOrderMap.delete(id) });
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap])
this.addPost(posts);
return posts
}
}
const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `a${this.orderKeyList.at(0)}` : `b${this.orderKeyList.at(-1)}` : undefined;
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags}, limit, beforeAfter);
const newPostOrderMap = new Map(posts.map(post => [post.id, post]));
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap])
this.addPost(posts);
if (!posts.length) {
this.finished = true;
if (!this.posts.size) this.events.fire('noPost');
else this.events.fire('endPost')
}
return posts
}
get orderKeyList() { return [...this.orderMap.keys()]}
}
interface $PostGridEventMap extends $LayoutEventMap {
startLoad: [];
noPost: [];
endPost: [];
post_error: [message: string];
}
interface FavoritesData {
id: id;
post_id: id;
user_id: id;
}

View File

@ -2,13 +2,16 @@ import { $Container, $Image, $State, $Video } from "elexis";
import type { Post } from "../../structure/Post";
import { time } from "../../structure/Util";
import { detailPanelEnable$ } from "../../main";
import type { $PostGrid } from "../PostGrid/$PostGrid";
export class $PostTile extends $Container {
post: Post;
$video: $Video | null;
$img: $Image;
duration$ = $.state(``);
constructor(post: Post) {
$grid: $PostGrid;
constructor($grid: $PostGrid, post: Post) {
super('post-tile');
this.$grid = $grid;
this.post = post;
this.$video = this.post.isVideo ? $('video').width(this.post.image_width).height(this.post.image_height).disablePictureInPicture(true).loop(true).muted(true).hide(true).on('mousedown', (e) => e.preventDefault()) : null;
this.$img = $('img').draggable(false).css({opacity: '0'}).width(this.post.image_width).height(this.post.image_height).src(this.post.previewURL).loading('lazy');
@ -35,7 +38,7 @@ export class $PostTile extends $Container {
$('span').content('GIF')
]) : null,
// Tile
$('a').href(this.post.pathname).preventDefault(detailPanelEnable$).content(() => [
$('a').href(this.url).preventDefault(detailPanelEnable$).content(() => [
this.$video,
this.$img.on('mousedown', (e) => e.preventDefault())
.once('load', (e, $img) => {
@ -67,4 +70,6 @@ export class $PostTile extends $Container {
const t = time(this.post.media_asset.duration * 1000 - this.$video.currentTime() * 1000)
this.duration$.set(Number(t.hh) > 0 ? `${t.hh}:${t.mm}:${t.ss}` : `${t.mm}:${t.ss}`)
}
get url() { return `${this.post.pathname}${this.$grid.tags ? `?q=${this.$grid.tags}` : ''}` }
}

View File

@ -112,9 +112,11 @@ $(document.body).content([
]),
$('div').class('no-post').hide(true).self($div => {
$div.on('startLoad', () => $div.hide(true))
$postGrid
$postGrid.self(() => {
$postGrid.posts.events
.on('noPost', () => $div.hide(false).content('No Posts'))
.on('post_error', message => $div.hide(false).content(message))
})
}),
$postGrid,
$detail

View File

@ -5,19 +5,22 @@ import { ClientUser } from "../../structure/ClientUser";
import { $VideoController } from "../../component/VideoController/$VideoController";
import { $Input } from "elexis/lib/node/$Input";
import { $DetailPanel } from "../../component/DetailPanel/$DetailPanel";
import { PostManager } from "../../structure/PostManager";
export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => {
export const post_route = $('route').path('/posts/:id?q').id('post').static(false).builder(({$route, params}) => {
if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
const post = Post.get(Booru.used, +params.id);
const $video = $('video');
const events = $.events<{
viewerPanel_hide: [],
viewerPanel_show: [],
viewerPanel_switch: [],
original_size: [],
video_play_pause: []
video_play_pause: [],
post_switch: [Post]
}>();
$.keys($(window))
let post: Post, posts: PostManager | undefined;
events.on('video_play_pause', () => { if ($video.isPlaying) $video.pause(); else $video.play() })
$.keys($(window)).self($keys => $keys
.if(e => {
if ($(e.target) instanceof $Input) return;
if (!$route.inDOM()) return;
@ -32,10 +35,44 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
if ($video.isPlaying) $video.pause();
else $video.play();
})
.keydown(['a', 'A'], e => navPost('prev') )
.keydown(['d', 'D'], e => { navPost('next') })
)
$route.on('open', ({params, query}) => {
posts = query.q?.includes('order:') ? undefined : PostManager.get(query.q);
post = Post.get(Booru.used, +params.id);
if (posts) {
if (!posts.orderMap.size || !posts.cache.has(post)) {
posts.cache.add(post);
posts.orderMap.set(post.id, post);
posts.fetchPosts('newer');
posts.fetchPosts('older');
} else {
const ordered = [...posts.orderMap.values()];
const index = ordered.indexOf(post);
if (!posts.finished && index > ordered.length - posts.limit / 2) {
posts.fetchPosts('older');
} else if (index === 0) {
posts.fetchPosts('newer');
}
}
}
events.fire('post_switch', post);
})
function navPost(dir: 'next' | 'prev') {
if (!posts) return;
const orderList = [...posts.orderMap.values()];
const index = orderList.indexOf(post);
if (dir === 'prev' && index === 0) return;
const targetPost = orderList.at(dir === 'next' ? index + 1 : index - 1);
if (!targetPost) return;
$.replace(`/posts/${targetPost.id}${posts.tags ? `?q=${posts.tags}` : ''}`)
}
return [
$('div').class('viewer').content(async ($viewer) => {
events.on('video_play_pause', () => { if ($video.isPlaying) $video.pause(); else $video.play() })
await post.ready;
$('div').class('viewer').self(async ($viewer) => {
$viewer
.on('pointermove', (e) => {
if (e.pointerType === 'mouse' || e.pointerType === 'pen') events.fire('viewerPanel_show');
@ -48,7 +85,9 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
.on('mouseleave', () => {
events.fire('viewerPanel_hide');
})
return [
events.on('post_switch', async post => {
await post.ready;
$viewer.content([
$('div').class('viewer-panel').hide(false)
.content([
$('div').class('panel').content([
@ -80,23 +119,32 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
}),
post.isVideo
? $video.height(post.image_height).width(post.image_width).src(post.file_ext === 'zip' ? post.large_file_url : post.file_url).controls(false).autoplay(true).loop(true).disablePictureInPicture(true)
: $('img').src(post.isLargeFile ? post.large_file_url : post.file_url).self($img => {
: $('img').height(post.image_height).width(post.image_width).self($img => {
$img.once('load', () =>
$img.once('load', () => $img.removeClass('loading')).src(post.isLargeFile ? post.large_file_url : post.file_url)
).src(post.preview_file_url)
if (!$img.complete) $img.class('loading')
events.on('original_size', () => $img.src(post.file_url))
})
]
])
})
}),
$('div').class('content').content([
$('h3').content(`Artist's Commentary`),
$('section').class('commentary').content(async ($comentary) => {
$('section').class('commentary').self(async ($comentary) => {
events.on('post_switch', async post => {
const commentary = (await ArtistCommentary.fetchMultiple(Booru.used, {post: {_id: post.id}})).at(0);
return [
$comentary.content([
commentary ? [
commentary.original_title ? $('h3').content(commentary.original_title) : null,
$('pre').content(commentary.original_description)
] : 'No commentary'
]
])
})
})
]),
new $DetailPanel().position($route).update(post)
new $DetailPanel().position($route).self($detail => {
events.on('post_switch', (post) => $detail.update(post))
})
]
})

View File

@ -25,7 +25,12 @@
img {
max-width: 100%;
max-height: 100%;
// transition: all 0.3s ease;
object-fit: contain;
transition: all 0.3s ease;
&.loading {
filter: blur(5px);
}
}
video {

View File

@ -17,7 +17,7 @@ export class Post extends $EventManager<{update: []}> {
score$ = $.state(0);
file_size$ = $.state(LOADING_STRING);
file_ext$ = $.state(LOADING_STRING);
file_url$ = $.state(LOADING_STRING);
file_url$ = $.state<string | undefined>(LOADING_STRING);
source$ = $.state(LOADING_STRING);
dimension$ = $.state(LOADING_STRING);
booruUrl$ = $.state(LOADING_STRING);
@ -61,13 +61,16 @@ export class Post extends $EventManager<{update: []}> {
}
const dataArray = await booru.fetch<PostData[]>(`/posts.json?limit=${limit}&tags=${tagsQuery}${page ? `&page=${page}` : ''}&_method=get`);
if (dataArray instanceof Array === false) return [];
const tagnameSet = new Set<string>();
const list = dataArray.map(data => {
const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id, data);
booru.posts.set(instance.id, instance);
instance.tag_string.split(' ').forEach(tag_name => tagnameSet.add(tag_name))
return instance;
});
if (!list.length) return list;
const userIds = [...new Set(dataArray.map(data => [data.approver_id, data.uploader_id].detype(null)).flat())];
// Tag.fetchMultiple(booru, {name: tagnameSet.array.toString().replaceAll(',', ' ')});
User.fetchMultiple(booru, {id: userIds}).then(() => list.forEach(post => post.update$()));
return list;
}
@ -97,7 +100,10 @@ export class Post extends $EventManager<{update: []}> {
async fetchTags() {
await this.ready;
return await Tag.fetchMultiple(this.booru, {name: {_space: this.tag_string}});
const uncached_tags = this.tag_string.split(' ').filter(tag_name => !Tag.get(this.booru, tag_name));
if (!uncached_tags.length) return this;
await Tag.fetchMultiple(this.booru, {name: {_space: uncached_tags.toString().replaceAll(',', ' ')}});
return this;
}
async createFavorite() {
@ -182,7 +188,7 @@ export interface PostData extends PostOptions {
"tag_string_copyright": string,
"tag_string_artist": string,
"tag_string_meta": string,
"file_url": string,
"file_url"?: string,
"large_file_url": string,
"preview_file_url": string
}

View File

@ -0,0 +1,105 @@
import { $EventManager } from "elexis";
import { Post } from "./Post";
import { Booru } from "./Booru";
export class PostManager {
static managers = new Map<string | undefined, PostManager>();
orderMap = new Map<id, Post>();
cache = new Set<Post>();
limit = 100;
tags?: string;
finished = false;
events = new $EventManager<PostManagerEventMap>();
constructor(tags?: string) {
this.tags = tags;
PostManager.managers.set(this.tags, this);
}
static get(tags: string | undefined) {
const manager = this.managers.get(tags) ?? new PostManager(tags);
this.managers.set(manager.tags, manager);
return manager;
}
clear() {
this.orderMap.clear();
this.cache.clear();
}
async fetchPosts(direction: 'newer' | 'older'): Promise<Post[]> {
const tags = this.tags ? decodeURIComponent(this.tags).split('+') : undefined;
const generalTags: string[] = [];
const orderTags: string[] = [];
let limit: number = this.limit;
let posts: Post[] = [];
if (tags) for (const tag of tags) {
if (tag.startsWith('ordfav:')) orderTags.push(tag);
else if (tag.startsWith('order:')) orderTags.push(tag);
else if (tag.startsWith('limit:')) limit = Number(tag.split(':')[1]);
else generalTags.push(tag);
}
if (orderTags.length) {
if (orderTags.length > 1) {
this.events.fire('post_error', `Error: These query can't be used together [${orderTags}].`)
return [];
}
const orderTag = orderTags[0];
if (orderTag.startsWith('ordfav:')) {
const username = orderTag.split(':')[1];
const match_tags = generalTags.length ? `&search[post_tags_match]=${generalTags.toString().replaceAll(',', '+')}` : '';
const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `&search[id]=>${this.orderKeyList.at(0)}` : `&search[id]=<${this.orderKeyList.at(-1)}` : undefined;
const favoritesDataList = await Booru.used.fetch<FavoritesData[]>(`/favorites.json?search[user_name]=${username}${beforeAfter ?? ''}${match_tags}&limit=${limit}`);
posts = await Post.fetchMultiple(Booru.used, {tags: `id:${favoritesDataList.map(data => data.post_id).toString()}`});
const newPostOrderMap = new Map();
for (const fav of favoritesDataList) {
const post = posts.find(post => post.id === fav.post_id);
if (!post) continue;
if (!post.file_url) continue;
newPostOrderMap.set(fav.id, post);
}
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap]);
this.events.fire('post_fetch', posts);
return posts;
}
if (orderTag.startsWith('order:')) {
const page = this.orderKeyList.length ? direction === 'newer' ? 1 : (this.orderMap.size / limit) + 1 : undefined;
posts = await Post.fetchMultiple(Booru.used, {tags: this.tags}, limit, page);
const newPostOrderMap = new Map(posts.filter(post => post.file_url).map(post => [post.id, post]));
newPostOrderMap.forEach((post, id) => { if (this.orderMap.has(id)) newPostOrderMap.delete(id) });
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap])
this.events.fire('post_fetch', posts);
}
} else {
const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `a${this.orderKeyList.at(0)}` : `b${this.orderKeyList.at(-1)}` : undefined;
posts = await Post.fetchMultiple(Booru.used, {tags: this.tags}, limit, beforeAfter);
const newPostOrderMap = new Map(posts.filter(post => post.file_url).map(post => [post.id, post]));
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap])
}
if (!posts.length) {
this.finished = true;
if (!this.cache.size) this.events.fire('noPost');
else this.events.fire('endPost')
}
this.events.fire('post_fetch', posts);
return posts
}
get orderKeyList() { return [...this.orderMap.keys()]}
}
interface PostManagerEventMap {
startLoad: [];
noPost: [];
endPost: [];
post_error: [message: string];
post_fetch: [Post[]];
}
interface FavoritesData {
id: id;
post_id: id;
user_id: id;
}

View File

@ -1,4 +1,4 @@
import type { Booru } from "./Booru";
import { Booru } from "./Booru";
export interface TagOptions {}
export interface Tag extends TagData {}
@ -41,6 +41,10 @@ export class Tag {
return list;
}
static get(booru: Booru, name: string) {
return [...booru.tags.values()].find(tag => tag.name === name);
}
update(data: TagData) {
Object.assign(this, data);
this.$update();