v0.11.0
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:
parent
eb2cbf688a
commit
dc176f4e83
1
dist/assets/index-D9u7pURH.js
vendored
1
dist/assets/index-D9u7pURH.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-DAA98Qle.js
vendored
Normal file
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
4
dist/index.html
vendored
@ -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>
|
||||
|
@ -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",
|
||||
|
23
src/component/$SlideViewer.ts
Normal file
23
src/component/$SlideViewer.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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;
|
||||
}
|
@ -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}` : ''}` }
|
||||
}
|
@ -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
|
||||
|
@ -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))
|
||||
})
|
||||
]
|
||||
})
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
105
src/structure/PostManager.ts
Normal file
105
src/structure/PostManager.ts
Normal 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;
|
||||
}
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user