v0.12.0
- 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:
parent
793d0342ff
commit
9d1d8fca39
File diff suppressed because one or more lines are too long
1
dist/assets/index-BtOtuQPY.js
vendored
1
dist/assets/index-BtOtuQPY.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-DOm6Phmh.js
vendored
Normal file
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
4
dist/index.html
vendored
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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) }
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
})
|
||||
}
|
||||
|
104
src/component/PostViewer/$PostViewer.ts
Normal file
104
src/component/PostViewer/$PostViewer.ts
Normal 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: [],
|
||||
}
|
@ -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;
|
||||
|
@ -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([
|
||||
|
@ -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%;
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user