- new - support new gesture:
  - double tap on video will do play/pause.
  - swipe left/right can switch post.
- new - $SlideViewer, $PostViewer.
This commit is contained in:
defaultkavy 2024-10-25 16:43:35 +08:00
parent 793d0342ff
commit 9d1d8fca39
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
14 changed files with 376 additions and 130 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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-BtOtuQPY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Plu04C2a.css">
<script type="module" crossorigin src="/assets/index-DOm6Phmh.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bmz9OSnh.css">
</head>
<body>
</body>

View File

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

View File

@ -1,23 +1,180 @@
import { $Container, $Element, $PointerManager } from "elexis";
import { $View } from "../../../elexis-ext/view";
import { $Container, $Element, $Node, $Pointer, $PointerManager, type $ContainerContentType, type $ContainerEventMap, type $EventMap } from "elexis";
export class $SlideViewer extends $Container {
export class $SlideViewer extends $Container<HTMLElement, $SlideViewerEventMap> {
pointers = new $PointerManager(this);
$container = $('div')
$container = $('div').class('slide-container')
slideMap = new Map<string | number, $Slide>();
slideId: null | string | number = null;
#pointerException?: (pointer: $Pointer, e: PointerEvent) => boolean;
constructor() {
super('slide-viewer')
super('slide-viewer');
this.css({position: 'relative'});
this.__build__();
new ResizeObserver(() => {
if (!this.inDOM()) return;
this.__render__();
this.trigger('resize');
}).observe(this.dom);
}
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})`});
this.$container.css({position: 'relative', height: '100%'})
let containerStartLeft = 0, containerLeft = 0;
this.pointers.on('down', ($pointer, e) => {
if (this.#pointerException) {
if (!this.#pointerException($pointer, e)) return $pointer.delete();
}
containerStartLeft = this.$container.offsetLeft;
})
this.pointers.on('move', ($pointer, e) => {
e.preventDefault();
containerLeft = containerStartLeft + $pointer.move_x;
if (containerLeft > containerStartLeft && this.slideList.at(0)?.slideId() === this.slideId) return;
if (containerLeft < containerStartLeft && this.slideList.at(-1)?.slideId() === this.slideId) return;
this.$container.css({left: `${containerLeft}px`});
})
this.pointers.on('up', ($pointer) => {
const width = this.domRect().width;
const containerMove = containerStartLeft - this.$container.offsetLeft;
if ($pointer.move_x === 0) return;
if ($pointer.movement_x < -5 || containerMove > width / 2) this.next();
else if ($pointer.movement_x > 5 || containerMove + width < width / 2) this.prev();
else {
containerLeft = containerStartLeft;
this.__slideAnimate__()
}
})
}
addSlide(id: string, $element: $Element) {
addSlides(slides: OrMatrix<$Slide>) {
slides = $.orArrayResolve(slides);
if (!slides.length) return;
for (const $slide of slides) {
if ($slide instanceof Array) this.addSlides($slide);
else {
this.slideMap.set($slide.slideId(), $slide);
this.$container.insert($slide)
}
}
this.__render__();
return this;
}
arrange(list: (string | number)[]) {
const newOrderedMap = new Map<string | number, $Slide>();
list.forEach(id => {
const $slide = this.slideMap.get(id);
if (!$slide) return;
newOrderedMap.set(id, $slide);
})
this.slideMap = newOrderedMap;
this.__render__();
return this;
}
switch(id: string | number | undefined) {
if (id === undefined) return this;
const $targetSlide = this.slideMap.get(id);
if (!$targetSlide) throw 'target undefined';
if ($targetSlide.slideId() === this.slideId) return this;
this.events.fire('beforeSwitch', {prevSlide: this.currentSlide, nextSlide: $targetSlide})
this.slideId = id;
this.__slideAnimate__();
this.events.fire('switch', {nextSlide: $targetSlide})
return this;
}
protected __slideAnimate__() {
const currentIndex = this.currentSlide ? this.slideList.indexOf(this.currentSlide) : undefined;
if (currentIndex === undefined) return;
const ease = Math.abs(this.getPositionLeft(currentIndex) - this.$container.offsetLeft) === this.dom.clientWidth;
this.$container.animate({
left: `-${this.getPositionLeft(currentIndex)}px`,
}, {
duration: 300,
easing: ease ? 'ease' : 'ease-out',
}, () => {
this.__render__();
})
}
protected __navigation__(dir: 'next' | 'prev') {
const currentSlide = this.currentSlide;
const slideList = this.slideList;
const currentIndex = currentSlide ? slideList.indexOf(currentSlide) : undefined;
if (currentIndex === undefined) { this.switch(slideList.at(0)?.slideId()); return this }
const targetIndex = $.call(() => {
switch (dir) {
case 'next': return currentIndex === slideList.length ? currentIndex : currentIndex + 1
case 'prev': return currentIndex === 0 ? currentIndex : currentIndex -1
}
})
const $targetSlide = this.slideList.at(targetIndex);
this.switch($targetSlide?.slideId());
return this;
}
next() { return this.__navigation__('next') }
prev() { return this.__navigation__('prev') }
get currentSlide() { return this.slideId ? this.slideMap.get(this.slideId) : undefined; }
get slideIdList() { return Array.from(this.slideMap.keys()); }
get slideList() { return Array.from(this.slideMap.values()); }
protected getPositionLeft(index: number) { return index * this.dom.clientWidth }
protected __render__() {
let i = 0;
this.slideMap.forEach($slide => {
$slide.hide(true, false);
$slide.css({top: '0', left: `${this.getPositionLeft(i)}px`});
i++;
})
if (!this.currentSlide) return;
const currentIndex = this.slideList.indexOf(this.currentSlide);
this.currentSlide.build().hide(false, false);
if (currentIndex !== 0) this.slideList.at(currentIndex - 1)?.build().hide(false, false);
if (currentIndex !== this.slideList.length - 1) this.slideList.at(currentIndex + 1)?.build().hide(false, false);
this.$container.children.render();
this.$container.css({left: `-${this.getPositionLeft(currentIndex)}px`})
}
pointerException(resolver: (pointer: $Pointer, e: PointerEvent) => boolean) {
this.#pointerException = resolver;
return this;
}
}
export interface $SlideViewerEventMap extends $ContainerEventMap {
switch: [{nextSlide: $Slide}];
beforeSwitch: [{prevSlide?: $Slide, nextSlide: $Slide}];
}
export class $Slide extends $Container {
#builder?: () => OrMatrix<$ContainerContentType>;
builded = false;
#slideId?: string | number;
constructor() {
super('slide');
this.css({width: '100%', height: '100%', display: 'block', position: 'absolute'})
}
builder(builder: () => OrMatrix<$ContainerContentType>) {
this.#builder = builder;
return this;
}
build() {
if (!this.builded && this.#builder) {
this.content(this.#builder());
this.builded = true;
}
return this;
}
slideId(): string | number;
slideId(slideId: string | number): this;
slideId(slideId?: string | number) { return $.fluent(this, arguments, () => this.#slideId, () => this.#slideId = slideId) }
}

View File

@ -57,7 +57,7 @@ export class $Drawer extends $Container {
])
this.pointers.on('move', pointer => {
if ($(':.viewer')?.contains(pointer.$target)) return;
if ($(':slide-viewer')?.contains(pointer.$target)) return;
pointer.$target.parent
if (pointer.type !== 'pen' && pointer.type !== 'touch') return;
if (pointer.move_y > 4 || pointer.move_y < -4) return;

View File

@ -9,7 +9,7 @@ interface $PostGridOptions {
tags?: string
}
export class $PostGrid extends $Layout {
$posts = new Map<Post, $PostTile>();
$postMap = new Map<Post, $PostTile>();
tags?: string;
$focus = $.focus();
posts: PostManager;
@ -23,7 +23,7 @@ export class $PostGrid extends $Layout {
}
protected async init() {
this.posts.events.on('post_fetch', (posts) => { this.addPost(posts) })
this.posts.events.on('post_fetch', (posts) => { this.renderPosts() })
setInterval(async () => { if (this.inDOM() && document.documentElement.scrollTop === 0) await this.posts.fetchPosts('newer'); }, 10000);
Booru.events.on('set', () => {
this.removeAll();
@ -38,8 +38,8 @@ export class $PostGrid extends $Layout {
$.keys($(window))
.if(e => {
if ($(e.target) instanceof $Input) return;
if (!this.inDOM()) return;
if ($(e.target) instanceof $Input) return;
return true;
})
// .keydown('Tab', e => {
@ -77,25 +77,19 @@ export class $PostGrid extends $Layout {
this.column(col >= 2 ? col : 2);
}
addPost(posts: OrArray<Post>) {
posts = $.orArrayResolve(posts);
for (const post of posts) {
if (!post.file_url) continue;
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.cache.add(post);
}
renderPosts() {
this.$focus.layer(100).elementSet.clear();
const $posts = [...this.posts.orderMap.values()].map(post => {
return this.$posts.get(post)?.self(this.$focus.layer(100).add)
const $postList = [...this.posts.orderMap.values()].map(post => {
const $post = this.$postMap.get(post) ?? new $PostTile(this, post).on('$focus', (e, $post) => this.$focus.layer(100).focus($post));
this.$postMap.set(post, $post)
return $post.self(this.$focus.layer(100).add)
});
this.content($posts).render();
this.content($postList).render();
return this;
}
removeAll() {
this.$posts.clear();
this.$postMap.clear();
this.$focus.layer(100).removeAll();
this.animate({opacity: [1, 0]}, {duration: 300, easing: 'ease'}, () => this.clear().render())
return this;

View File

@ -59,8 +59,8 @@ export class $PostTile extends $Container {
}, {passive: true} )
.on('click', () => {
if (!detailPanelEnable$.value) return;
if (innerWidth <= 800) return $.open(this.post.pathname);
if (this.attribute('focus') === '') $.open(this.post.pathname);
if (innerWidth <= 800) return $.open(this.url);
if (this.attribute('focus') === '') $.open(this.url);
else this.trigger('$focus');
})
}

View File

@ -0,0 +1,104 @@
import { $Container } from "elexis";
import type { Post } from "../../structure/Post";
import { Booru } from "../../structure/Booru";
import { ClientUser } from "../../structure/ClientUser";
import { $VideoController } from "../VideoController/$VideoController";
import { $Input } from "elexis/lib/node/$Input";
export class $PostViewer extends $Container<HTMLElement, $PostViewerEventMap> {
$video = $('video');
post: Post;
constructor(post: Post) {
super('div');
this.post = post
this.class('viewer');
this.build();
}
async build() {
await this.post.ready;
this.events.on('video_play_pause', () => { if (this.$video.isPlaying) this.$video.pause(); else this.$video.play() })
this.content([
$('div').class('viewer-panel').hide(false).content($viewerPanel => {
this.events.on('viewerPanel_hide', () => $viewerPanel.hide(true))
.on('viewerPanel_show', () => $viewerPanel.hide(false))
.on('viewerPanel_switch', () => { $viewerPanel.hide(!$viewerPanel.hide()) })
return [
$('div').class('panel').content([
this.post.isVideo ? new $VideoController(this.$video, this, this.post) : null,
$('div').class('buttons').content([
$('ion-icon').title('Favorite').name('heart-outline').self($heart => {
ClientUser.events.on('favoriteUpdate', (user) => {
if (user.favorites.has(this.post.id)) $heart.name('heart');
else $heart.name('heart-outline');
})
if (Booru.used.user?.favorites.has(this.post.id)) $heart.name('heart');
$heart.on('click', () => {
if (Booru.used.user?.favorites.has(this.post.id)) this.post.deleteFavorite();
else this.post.createFavorite();
})
}),
$('ion-icon').title('Original Size').name('resize-outline').self($original => {
$original.on('click', () => { this.events.fire('original_size'); $original.disable(true); })
if (!this.post.isLargeFile || this.post.isVideo) $original.disable(true);
})
])
]),
$('div').class('overlay')
]
}),
this.post.isVideo
? this.$video.height(this.post.image_height).width(this.post.image_width).src(this.post.file_ext === 'zip' ? this.post.large_file_url : this.post.file_url)
.controls(false).loop(true).disablePictureInPicture(true)
: $('img').height(this.post.image_height).width(this.post.image_width).self($img => {
$img.once('load', () =>
$img.once('load', () => $img.removeClass('loading')).src(this.post.isLargeFile ? this.post.large_file_url : this.post.file_url)
).src(this.post.preview_file_url)
if (!$img.complete) $img.class('loading')
this.events.on('original_size', () => $img.src(this.post.file_url))
})
])
this.on('pointerleave', (e) => {
if (e.pointerType === 'touch') return;
this.events.fire('viewerPanel_hide');
})
this.on('pointermove', (e) => {
if (e.pointerType === 'mouse' || e.pointerType === 'pen') this.events.fire('viewerPanel_show');
})
let doubleTap: Timer | null = null;
$.pointers(this)
.on('up', pointer => {
if ( this.$(':.viewer-panel .panel')?.contains($(pointer.$target)) ) return;
if (pointer.type === 'mouse') this.events.fire('video_play_pause');
else {
if (doubleTap !== null) {
this.events.fire('video_play_pause');
}
doubleTap = setTimeout(() => {
doubleTap = null;
}, 300);
this.events.fire('viewerPanel_switch');
}
})
$.keys($(window)).self($keys => $keys
.if(e => {
if ($(e.target) instanceof $Input) return;
if (!this.inDOM()) return;
return true;
})
.keydown(' ', e => {
e.preventDefault();
if (this.$video.isPlaying) this.$video.pause();
else this.$video.play();
})
)
}
}
export interface $PostViewerEventMap {
viewerPanel_hide: [],
viewerPanel_show: [],
viewerPanel_switch: [],
original_size: [],
video_play_pause: [],
}

View File

@ -1,13 +1,14 @@
import { $Container, $Node, type $Video } from "elexis";
import { time } from "../../structure/Util";
import type { Post } from "../../structure/Post";
import type { $PostViewer } from "../PostViewer/$PostViewer";
export class $VideoController extends $Container {
$video: $Video;
$viewer: $Container;
duration$ = $.state('00:00');
post: Post;
constructor($video: $Video, $viewer: $Container, post: Post) {
constructor($video: $Video, $viewer: $PostViewer, post: Post) {
super('video-controller')
this.$video = $video
this.$viewer = $viewer;

View File

@ -1,25 +1,19 @@
import { Post } from "../../structure/Post";
import { ArtistCommentary } from "../../structure/Commentary";
import { Booru } from "../../structure/Booru";
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";
import { $PostViewer } from "../../component/PostViewer/$PostViewer";
import { $Slide, $SlideViewer } from "../../component/$SlideViewer";
import { $Video } from "elexis";
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 $video = $('video');
const events = $.events<{
viewerPanel_hide: [],
viewerPanel_show: [],
viewerPanel_switch: [],
original_size: [],
video_play_pause: [],
post_switch: [Post]
}>();
let post: Post, posts: PostManager | undefined;
events.on('video_play_pause', () => { if ($video.isPlaying) $video.pause(); else $video.play() })
let post: Post, posts: PostManager;
$.keys($(window)).self($keys => $keys
.if(e => {
if ($(e.target) instanceof $Input) return;
@ -30,103 +24,76 @@ export const post_route = $('route').path('/posts/:id?q').id('post').static(fals
if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite();
else post.createFavorite();
})
.keydown(' ', e => {
e.preventDefault();
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);
const $slideViewerMap = new Map<string | undefined, $SlideViewer>();
$route.on('open', async ({params, query}) => {
posts = PostManager.get(query.q);
post = Post.get(Booru.used, +params.id);
if (posts) {
posts.events.on('post_fetch', slideViewerHandler);
if (!posts.orderMap.size || !posts.cache.has(post)) {
posts.cache.add(post);
await post.ready
posts.addPosts(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) {
if (!posts.finished && index === ordered.length - 1) {
posts.fetchPosts('older');
} else if (index === 0) {
posts.fetchPosts('newer');
}
}
}
slideViewerHandler({manager: posts});
const $slideViewer = $getSlideViewer(posts.tags)
$slideViewer.switch(post.id);
events.fire('post_switch', post);
})
function $getSlideViewer(q: string | undefined) {
const $slideViewer = $slideViewerMap.get(q) ??
new $SlideViewer()
.pointerException((pointer) => {
if ($slideViewer.currentSlide?.$('::.progressbar-container').find($div => $div.contains(pointer.$target))) return false;
if (pointer.type === 'mouse') return false;
return true;
})
.on('switch', ({nextSlide: $target}) => {
$.replace(`/posts/${$target.slideId()}${q ? `?q=${q}` : ''}`);
}).on('beforeSwitch', ({prevSlide, nextSlide}) => {
const $prevVideo = prevSlide?.$<$Video>(':video');
if ($prevVideo?.isPlaying) $prevVideo.pause();
const $nextVideo = nextSlide.$<$Video>(':video');
if ($nextVideo?.isPlaying === false) $nextVideo.play();
})
$slideViewerMap.set(q, $slideViewer);
return $slideViewer;
}
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}` : ''}`)
$.replace(`/posts/${targetPost.id}${posts.tags ? `?q=${posts.tags}` : ''}`);
}
function slideViewerHandler(params: {manager: PostManager}) {
const { manager: posts } = params;
const $slideViewer = $getSlideViewer(posts.tags);
const postList = posts.cache.array.filter(post => !$slideViewer.slideMap.has(post.id));
$slideViewer.addSlides(postList.map(post => new $Slide().slideId(post.id).builder(() => new $PostViewer(post))));
if (postList.length) $slideViewer.arrange([...posts.orderMap.values()].map(post => post.id));
}
return [
$('div').class('viewer').self(async ($viewer) => {
$viewer
.on('pointermove', (e) => {
if (e.pointerType === 'mouse' || e.pointerType === 'pen') events.fire('viewerPanel_show');
})
.on('pointerup', (e) => {
if ( $(':.viewer-panel .panel')?.contains($(e.target)) ) return;
if (e.pointerType === 'touch') events.fire('viewerPanel_switch');
if (e.pointerType === 'mouse') events.fire('video_play_pause');
})
.on('mouseleave', () => {
events.fire('viewerPanel_hide');
})
events.on('post_switch', async post => {
await post.ready;
$viewer.content([
$('div').class('viewer-panel').hide(false)
.content([
$('div').class('panel').content([
post.isVideo ? new $VideoController($video, $viewer, post) : null,
$('div').class('buttons').content([
$('ion-icon').title('Favorite').name('heart-outline').self($heart => {
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);
})
])
]),
$('div').class('overlay')
])
.self($viewerPanel => {
events.on('viewerPanel_hide', () => $viewerPanel.hide(true))
.on('viewerPanel_show', () => $viewerPanel.hide(false))
.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').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('slide-viewer-container').self($div => {
$route.on('open', () => {
$div.content($getSlideViewer(posts.tags))
})
}),
$('div').class('content').content([

View File

@ -2,11 +2,9 @@
padding: 0;
padding-top: var(--nav-height);
div.viewer {
slide-viewer {
display: block;
height: calc(100dvh - 2rem - var(--nav-height));
display: flex;
justify-content: center;
align-items: center;
background-color: #000000;
border-radius: var(--border-radius-large);
overflow: hidden;
@ -14,6 +12,7 @@
margin: 1rem;
position: relative;
transition: all 0.3s ease;
touch-action: none;
@media (max-width: 800px) {
width: 100%;
@ -21,6 +20,19 @@
border-radius: 0;
margin:0;
}
}
div.viewer {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000000;
border-radius: var(--border-radius-large);
overflow: hidden;
position: relative;
transition: all 0.3s ease;
img {
max-width: 100%;

View File

@ -32,6 +32,16 @@ export class PostManager {
this.cache.clear();
}
addPosts(posts: OrArray<Post>) {
posts = $.orArrayResolve(posts);
for (const post of posts) {
if (!post.file_url) continue;
if (this.cache.has(post)) continue;
this.cache.add(post);
}
return this;
}
async fetchPosts(direction: 'newer' | 'older'): Promise<Post[]> {
const tags = this.tags ? decodeURIComponent(this.tags).split('+') : undefined;
const generalTags: string[] = [];
@ -64,7 +74,7 @@ export class PostManager {
newPostOrderMap.set(fav.id, post);
}
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap]);
this.events.fire('post_fetch', posts);
this.events.fire('post_fetch', {manager: this, postList: posts});
return posts;
}
@ -74,7 +84,7 @@ export class PostManager {
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);
this.events.fire('post_fetch', {manager: this, postList: posts});
}
} else {
const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `a${this.orderKeyList.at(0)}` : `b${this.orderKeyList.at(-1)}` : undefined;
@ -89,7 +99,8 @@ export class PostManager {
else this.events.fire('endPost')
}
this.events.fire('post_fetch', posts);
this.events.fire('post_fetch', {manager: this, postList: posts});
this.addPosts(posts);
return posts
}
@ -101,7 +112,7 @@ interface PostManagerEventMap {
noPost: [];
endPost: [];
post_error: [message: string];
post_fetch: [Post[]];
post_fetch: [{manager: PostManager, postList: Post[]}];
}
interface FavoritesData {