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

View File

@ -2,7 +2,7 @@
"name": "danbooru-viewer", "name": "danbooru-viewer",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "0.10.5", "version": "0.11.0",
"scripts": { "scripts": {
"dev": "bun x vite", "dev": "bun x vite",
"build": "bun x vite build", "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('favorites').name('Favorites').content(this.post.favcount$),
new $Property('score').name('Score').content(this.post.score$) new $Property('score').name('Score').content(this.post.score$)
]), ]),
new $Property('file-url').name('File').content([ 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.replace('https://', ''))).target('_blank'), $('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)) $('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.file_url!))
]), ]) : null,
new $Property('source-url').name('Source').content([ new $Property('source-url').name('Source').content([
$('a').href(this.post.source$).content(this.post.source$.convert((value) => value.replace('https://', ''))).target('_blank'), $('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)) $('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 => { $('div').class('post-tags').content(async $tags => {
if (this.options.tagsType === 'detail') { 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] = [ const [artist_tags, char_tags, gen_tags, meta_tags, copy_tags] = [
tags.filter(tag => tag.category === TagCategory.Artist), tags.filter(tag => tag.category === TagCategory.Artist),
tags.filter(tag => tag.category === TagCategory.Character), tags.filter(tag => tag.category === TagCategory.Character),

View File

@ -3,33 +3,32 @@ import { Booru } from "../../structure/Booru";
import { Post } from "../../structure/Post"; import { Post } from "../../structure/Post";
import { $PostTile } from "../PostTile/$PostTile"; import { $PostTile } from "../PostTile/$PostTile";
import { $Input } from "elexis/lib/node/$Input"; import { $Input } from "elexis/lib/node/$Input";
import { PostManager } from "../../structure/PostManager";
interface $PostGridOptions { interface $PostGridOptions {
tags?: string tags?: string
} }
export class $PostGrid extends $Layout<$PostGridEventMap> { export class $PostGrid extends $Layout {
posts = new Set<Post>();
$posts = new Map<Post, $PostTile>(); $posts = new Map<Post, $PostTile>();
orderMap = new Map<id, Post>();
tags?: string; tags?: string;
finished = false;
limit = 100;
$focus = $.focus(); $focus = $.focus();
posts: PostManager;
constructor(options?: $PostGridOptions) { constructor(options?: $PostGridOptions) {
super(); super();
this.tags = options?.tags; this.tags = options?.tags;
this.posts = PostManager.get(this.tags);
this.addStaticClass('post-grid'); this.addStaticClass('post-grid');
this.type('waterfall').gap(10); this.type('waterfall').gap(10);
this.init(); this.init();
} }
protected async 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', () => { Booru.events.on('set', () => {
this.removeAll(); this.removeAll();
if (this.finished) { if (this.posts.finished) {
this.finished = false; this.posts.finished = false;
this.events.fire('startLoad');
this.loader(); this.loader();
} }
}) })
@ -37,7 +36,6 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
// this.on('afterRender', () => { // this.on('afterRender', () => {
// this.$focus.currentLayer?.focus(this.$focus.currentLayer.currentFocus); // this.$focus.currentLayer?.focus(this.$focus.currentLayer.currentFocus);
// }) // })
this.events.fire('startLoad');
this.loader(); this.loader();
this.$focus.layer(100).loop(false).scrollThreshold($.rem(2) + 60); this.$focus.layer(100).loop(false).scrollThreshold($.rem(2) + 60);
@ -59,7 +57,7 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
.keydown([' ', 'Enter'], e => { .keydown([' ', 'Enter'], e => {
e.preventDefault(); e.preventDefault();
const focused = this.$focus.currentLayer?.currentFocus; 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(); }) .keydown(['Escape'], e => { e.preventDefault(); this.$focus.blur(); })
} }
@ -67,11 +65,11 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
protected async loader() { protected async loader() {
if (!this.inDOM()) return setTimeout(() => this.loader(), 100);; if (!this.inDOM()) return setTimeout(() => this.loader(), 100);;
while (this.inDOM() && document.documentElement.scrollHeight <= innerHeight * 2) { 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 (!posts.length) return;
} }
if (document.documentElement.scrollTop + innerHeight > document.documentElement.scrollHeight - innerHeight * 2) { 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; if (!posts.length) return;
} }
setTimeout(() => this.loader(), 100); setTimeout(() => this.loader(), 100);
@ -86,13 +84,13 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
posts = $.orArrayResolve(posts); posts = $.orArrayResolve(posts);
for (const post of posts) { for (const post of posts) {
if (!post.file_url) continue; if (!post.file_url) continue;
if (this.posts.has(post)) continue; if (this.posts.cache.has(post)) continue;
const $post = new $PostTile(post).on('$focus', (e, $post) => this.$focus.layer(100).focus($post)); const $post = new $PostTile(this, post).on('$focus', (e, $post) => this.$focus.layer(100).focus($post));
this.$posts.set(post, $post); this.$posts.set(post, $post);
this.posts.add(post); this.posts.cache.add(post);
} }
this.$focus.layer(100).elementSet.clear(); 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) return this.$posts.get(post)?.self(this.$focus.layer(100).add)
}); });
this.content($posts).render(); this.content($posts).render();
@ -102,85 +100,9 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
removeAll() { removeAll() {
this.posts.clear(); this.posts.clear();
this.$posts.clear(); this.$posts.clear();
this.orderMap.clear();
this.$focus.layer(100).removeAll(); this.$focus.layer(100).removeAll();
this.animate({opacity: [1, 0]}, {duration: 300, easing: 'ease'}, () => this.clear().render()) this.animate({opacity: [1, 0]}, {duration: 300, easing: 'ease'}, () => this.clear().render())
return this; 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 type { Post } from "../../structure/Post";
import { time } from "../../structure/Util"; import { time } from "../../structure/Util";
import { detailPanelEnable$ } from "../../main"; import { detailPanelEnable$ } from "../../main";
import type { $PostGrid } from "../PostGrid/$PostGrid";
export class $PostTile extends $Container { export class $PostTile extends $Container {
post: Post; post: Post;
$video: $Video | null; $video: $Video | null;
$img: $Image; $img: $Image;
duration$ = $.state(``); duration$ = $.state(``);
constructor(post: Post) { $grid: $PostGrid;
constructor($grid: $PostGrid, post: Post) {
super('post-tile'); super('post-tile');
this.$grid = $grid;
this.post = post; 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.$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'); 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') $('span').content('GIF')
]) : null, ]) : null,
// Tile // Tile
$('a').href(this.post.pathname).preventDefault(detailPanelEnable$).content(() => [ $('a').href(this.url).preventDefault(detailPanelEnable$).content(() => [
this.$video, this.$video,
this.$img.on('mousedown', (e) => e.preventDefault()) this.$img.on('mousedown', (e) => e.preventDefault())
.once('load', (e, $img) => { .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) 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}`) 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').class('no-post').hide(true).self($div => {
$div.on('startLoad', () => $div.hide(true)) $div.on('startLoad', () => $div.hide(true))
$postGrid $postGrid.self(() => {
.on('noPost', () => $div.hide(false).content('No Posts')) $postGrid.posts.events
.on('post_error', message => $div.hide(false).content(message)) .on('noPost', () => $div.hide(false).content('No Posts'))
.on('post_error', message => $div.hide(false).content(message))
})
}), }),
$postGrid, $postGrid,
$detail $detail

View File

@ -5,19 +5,22 @@ import { ClientUser } from "../../structure/ClientUser";
import { $VideoController } from "../../component/VideoController/$VideoController"; import { $VideoController } from "../../component/VideoController/$VideoController";
import { $Input } from "elexis/lib/node/$Input"; import { $Input } from "elexis/lib/node/$Input";
import { $DetailPanel } from "../../component/DetailPanel/$DetailPanel"; 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'); if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
const post = Post.get(Booru.used, +params.id);
const $video = $('video'); const $video = $('video');
const events = $.events<{ const events = $.events<{
viewerPanel_hide: [], viewerPanel_hide: [],
viewerPanel_show: [], viewerPanel_show: [],
viewerPanel_switch: [], viewerPanel_switch: [],
original_size: [], 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 => {
if ($(e.target) instanceof $Input) return; if ($(e.target) instanceof $Input) return;
if (!$route.inDOM()) 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(); if ($video.isPlaying) $video.pause();
else $video.play(); 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 [ return [
$('div').class('viewer').content(async ($viewer) => { $('div').class('viewer').self(async ($viewer) => {
events.on('video_play_pause', () => { if ($video.isPlaying) $video.pause(); else $video.play() })
await post.ready;
$viewer $viewer
.on('pointermove', (e) => { .on('pointermove', (e) => {
if (e.pointerType === 'mouse' || e.pointerType === 'pen') events.fire('viewerPanel_show'); if (e.pointerType === 'mouse' || e.pointerType === 'pen') events.fire('viewerPanel_show');
@ -48,55 +85,66 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
.on('mouseleave', () => { .on('mouseleave', () => {
events.fire('viewerPanel_hide'); events.fire('viewerPanel_hide');
}) })
return [ events.on('post_switch', async post => {
$('div').class('viewer-panel').hide(false) await post.ready;
.content([ $viewer.content([
$('div').class('panel').content([ $('div').class('viewer-panel').hide(false)
post.isVideo ? new $VideoController($video, $viewer, post) : null, .content([
$('div').class('buttons').content([ $('div').class('panel').content([
$('ion-icon').title('Favorite').name('heart-outline').self($heart => { post.isVideo ? new $VideoController($video, $viewer, post) : null,
ClientUser.events.on('favoriteUpdate', (user) => { $('div').class('buttons').content([
if (user.favorites.has(post.id)) $heart.name('heart'); $('ion-icon').title('Favorite').name('heart-outline').self($heart => {
else $heart.name('heart-outline'); ClientUser.events.on('favoriteUpdate', (user) => {
if (user.favorites.has(post.id)) $heart.name('heart');
else $heart.name('heart-outline');
})
if (Booru.used.user?.favorites.has(post.id)) $heart.name('heart');
$heart.on('click', () => {
if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite();
else post.createFavorite();
})
}),
$('ion-icon').title('Original Size').name('resize-outline').self($original => {
$original.on('click', () => { events.fire('original_size'); $original.disable(true); })
if (!post.isLargeFile || post.isVideo) $original.disable(true);
}) })
if (Booru.used.user?.favorites.has(post.id)) $heart.name('heart'); ])
$heart.on('click', () => { ]),
if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite(); $('div').class('overlay')
else post.createFavorite(); ])
}) .self($viewerPanel => {
}), events.on('viewerPanel_hide', () => $viewerPanel.hide(true))
$('ion-icon').title('Original Size').name('resize-outline').self($original => { .on('viewerPanel_show', () => $viewerPanel.hide(false))
$original.on('click', () => { events.fire('original_size'); $original.disable(true); }) .on('viewerPanel_switch', () => $viewerPanel.hide(!$viewerPanel.hide()))
if (!post.isLargeFile || post.isVideo) $original.disable(true); }),
}) 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').height(post.image_height).width(post.image_width).self($img => {
$('div').class('overlay') $img.once('load', () =>
]) $img.once('load', () => $img.removeClass('loading')).src(post.isLargeFile ? post.large_file_url : post.file_url)
.self($viewerPanel => { ).src(post.preview_file_url)
events.on('viewerPanel_hide', () => $viewerPanel.hide(true)) if (!$img.complete) $img.class('loading')
.on('viewerPanel_show', () => $viewerPanel.hide(false)) events.on('original_size', () => $img.src(post.file_url))
.on('viewerPanel_switch', () => $viewerPanel.hide(!$viewerPanel.hide())) })
}), ])
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 => {
events.on('original_size', () => $img.src(post.file_url))
})
]
}), }),
$('div').class('content').content([ $('div').class('content').content([
$('h3').content(`Artist's Commentary`), $('h3').content(`Artist's Commentary`),
$('section').class('commentary').content(async ($comentary) => { $('section').class('commentary').self(async ($comentary) => {
const commentary = (await ArtistCommentary.fetchMultiple(Booru.used, {post: {_id: post.id}})).at(0); events.on('post_switch', async post => {
return [ const commentary = (await ArtistCommentary.fetchMultiple(Booru.used, {post: {_id: post.id}})).at(0);
commentary ? [ $comentary.content([
commentary.original_title ? $('h3').content(commentary.original_title) : null, commentary ? [
$('pre').content(commentary.original_description) commentary.original_title ? $('h3').content(commentary.original_title) : null,
] : 'No commentary' $('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 { img {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
// transition: all 0.3s ease; object-fit: contain;
transition: all 0.3s ease;
&.loading {
filter: blur(5px);
}
} }
video { video {

View File

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