- 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'); gtag('config', 'G-59HBGP98WR');
</script> </script>
<script type="module" crossorigin src="/assets/index-BtOtuQPY.js"></script> <script type="module" crossorigin src="/assets/index-DOm6Phmh.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Plu04C2a.css"> <link rel="stylesheet" crossorigin href="/assets/index-Bmz9OSnh.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.11.3", "version": "0.12.0",
"scripts": { "scripts": {
"dev": "bun x vite", "dev": "bun x vite",
"build": "bun x vite build", "build": "bun x vite build",

View File

@ -1,23 +1,180 @@
import { $Container, $Element, $PointerManager } from "elexis"; import { $Container, $Element, $Node, $Pointer, $PointerManager, type $ContainerContentType, type $ContainerEventMap, type $EventMap } from "elexis";
import { $View } from "../../../elexis-ext/view";
export class $SlideViewer extends $Container { export class $SlideViewer extends $Container<HTMLElement, $SlideViewerEventMap> {
pointers = new $PointerManager(this); 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() { constructor() {
super('slide-viewer') super('slide-viewer');
this.css({position: 'relative'});
this.__build__(); this.__build__();
new ResizeObserver(() => {
if (!this.inDOM()) return;
this.__render__();
this.trigger('resize');
}).observe(this.dom);
} }
protected __build__() { protected __build__() {
this.content([ this.$container ]); this.content([ this.$container ]);
this.pointers.on('move', $pointer => { this.$container.css({position: 'relative', height: '100%'})
const [x, y] = [$pointer.move_x, $pointer.move_y]; let containerStartLeft = 0, containerLeft = 0;
this.$container.css({transform: `translate(${x}, ${y})`}); 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; 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 => { this.pointers.on('move', pointer => {
if ($(':.viewer')?.contains(pointer.$target)) return; if ($(':slide-viewer')?.contains(pointer.$target)) return;
pointer.$target.parent pointer.$target.parent
if (pointer.type !== 'pen' && pointer.type !== 'touch') return; if (pointer.type !== 'pen' && pointer.type !== 'touch') return;
if (pointer.move_y > 4 || pointer.move_y < -4) return; if (pointer.move_y > 4 || pointer.move_y < -4) return;

View File

@ -9,7 +9,7 @@ interface $PostGridOptions {
tags?: string tags?: string
} }
export class $PostGrid extends $Layout { export class $PostGrid extends $Layout {
$posts = new Map<Post, $PostTile>(); $postMap = new Map<Post, $PostTile>();
tags?: string; tags?: string;
$focus = $.focus(); $focus = $.focus();
posts: PostManager; posts: PostManager;
@ -23,7 +23,7 @@ export class $PostGrid extends $Layout {
} }
protected async init() { 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); 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();
@ -38,8 +38,8 @@ export class $PostGrid extends $Layout {
$.keys($(window)) $.keys($(window))
.if(e => { .if(e => {
if ($(e.target) instanceof $Input) return;
if (!this.inDOM()) return; if (!this.inDOM()) return;
if ($(e.target) instanceof $Input) return;
return true; return true;
}) })
// .keydown('Tab', e => { // .keydown('Tab', e => {
@ -77,25 +77,19 @@ export class $PostGrid extends $Layout {
this.column(col >= 2 ? col : 2); this.column(col >= 2 ? col : 2);
} }
addPost(posts: OrArray<Post>) { renderPosts() {
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);
}
this.$focus.layer(100).elementSet.clear(); this.$focus.layer(100).elementSet.clear();
const $posts = [...this.posts.orderMap.values()].map(post => { const $postList = [...this.posts.orderMap.values()].map(post => {
return this.$posts.get(post)?.self(this.$focus.layer(100).add) 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; return this;
} }
removeAll() { removeAll() {
this.$posts.clear(); this.$postMap.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;

View File

@ -59,8 +59,8 @@ export class $PostTile extends $Container {
}, {passive: true} ) }, {passive: true} )
.on('click', () => { .on('click', () => {
if (!detailPanelEnable$.value) return; if (!detailPanelEnable$.value) return;
if (innerWidth <= 800) return $.open(this.post.pathname); if (innerWidth <= 800) return $.open(this.url);
if (this.attribute('focus') === '') $.open(this.post.pathname); if (this.attribute('focus') === '') $.open(this.url);
else this.trigger('$focus'); 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 { $Container, $Node, type $Video } from "elexis";
import { time } from "../../structure/Util"; import { time } from "../../structure/Util";
import type { Post } from "../../structure/Post"; import type { Post } from "../../structure/Post";
import type { $PostViewer } from "../PostViewer/$PostViewer";
export class $VideoController extends $Container { export class $VideoController extends $Container {
$video: $Video; $video: $Video;
$viewer: $Container; $viewer: $Container;
duration$ = $.state('00:00'); duration$ = $.state('00:00');
post: Post; post: Post;
constructor($video: $Video, $viewer: $Container, post: Post) { constructor($video: $Video, $viewer: $PostViewer, post: Post) {
super('video-controller') super('video-controller')
this.$video = $video this.$video = $video
this.$viewer = $viewer; this.$viewer = $viewer;

View File

@ -1,25 +1,19 @@
import { Post } from "../../structure/Post"; import { Post } from "../../structure/Post";
import { ArtistCommentary } from "../../structure/Commentary"; import { ArtistCommentary } from "../../structure/Commentary";
import { Booru } from "../../structure/Booru"; import { Booru } from "../../structure/Booru";
import { ClientUser } from "../../structure/ClientUser";
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"; 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}) => { 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 $video = $('video');
const events = $.events<{ const events = $.events<{
viewerPanel_hide: [],
viewerPanel_show: [],
viewerPanel_switch: [],
original_size: [],
video_play_pause: [],
post_switch: [Post] post_switch: [Post]
}>(); }>();
let post: Post, posts: PostManager | undefined; let post: Post, posts: PostManager;
events.on('video_play_pause', () => { if ($video.isPlaying) $video.pause(); else $video.play() })
$.keys($(window)).self($keys => $keys $.keys($(window)).self($keys => $keys
.if(e => { .if(e => {
if ($(e.target) instanceof $Input) return; 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(); if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite();
else post.createFavorite(); else post.createFavorite();
}) })
.keydown(' ', e => {
e.preventDefault();
if ($video.isPlaying) $video.pause();
else $video.play();
})
.keydown(['a', 'A'], e => navPost('prev') ) .keydown(['a', 'A'], e => navPost('prev') )
.keydown(['d', 'D'], e => { navPost('next') }) .keydown(['d', 'D'], e => { navPost('next') })
) )
const $slideViewerMap = new Map<string | undefined, $SlideViewer>();
$route.on('open', ({params, query}) => { $route.on('open', async ({params, query}) => {
posts = query.q?.includes('order:') ? undefined : PostManager.get(query.q); posts = PostManager.get(query.q);
post = Post.get(Booru.used, +params.id); post = Post.get(Booru.used, +params.id);
if (posts) { posts.events.on('post_fetch', slideViewerHandler);
if (!posts.orderMap.size || !posts.cache.has(post)) { if (!posts.orderMap.size || !posts.cache.has(post)) {
posts.cache.add(post); await post.ready
posts.orderMap.set(post.id, post); posts.addPosts(post);
posts.fetchPosts('newer'); 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 - 1) {
posts.fetchPosts('older'); posts.fetchPosts('older');
} else { } else if (index === 0) {
const ordered = [...posts.orderMap.values()]; posts.fetchPosts('newer');
const index = ordered.indexOf(post);
if (!posts.finished && index > ordered.length - posts.limit / 2) {
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); 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') { function navPost(dir: 'next' | 'prev') {
if (!posts) return;
const orderList = [...posts.orderMap.values()]; const orderList = [...posts.orderMap.values()];
const index = orderList.indexOf(post); const index = orderList.indexOf(post);
if (dir === 'prev' && index === 0) return; if (dir === 'prev' && index === 0) return;
const targetPost = orderList.at(dir === 'next' ? index + 1 : index - 1); const targetPost = orderList.at(dir === 'next' ? index + 1 : index - 1);
if (!targetPost) return; 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 [ return [
$('div').class('viewer').self(async ($viewer) => { $('div').class('slide-viewer-container').self($div => {
$viewer $route.on('open', () => {
.on('pointermove', (e) => { $div.content($getSlideViewer(posts.tags))
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('content').content([ $('div').class('content').content([

View File

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

View File

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