v0.11.0
change - depart PostManager methods from $PostGrid. new - navigation posts on post page. optimize - fetch fewer tags when tags is cached. change - Post.file_url and Post.file_url$ can be undefined. change: $PostTile need $PostGrid as params when construct. change: post page viewer $img will load preview image first. change: post page become non-static page.
This commit is contained in:
parent
eb2cbf688a
commit
dc176f4e83
1
dist/assets/index-D9u7pURH.js
vendored
1
dist/assets/index-D9u7pURH.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-DAA98Qle.js
vendored
Normal file
1
dist/assets/index-DAA98Qle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
gtag('config', 'G-59HBGP98WR');
|
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>
|
||||||
|
@ -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",
|
||||||
|
23
src/component/$SlideViewer.ts
Normal file
23
src/component/$SlideViewer.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { $Container, $Element, $PointerManager } from "elexis";
|
||||||
|
import { $View } from "../../../elexis-ext/view";
|
||||||
|
|
||||||
|
export class $SlideViewer extends $Container {
|
||||||
|
pointers = new $PointerManager(this);
|
||||||
|
$container = $('div')
|
||||||
|
constructor() {
|
||||||
|
super('slide-viewer')
|
||||||
|
this.__build__();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected __build__() {
|
||||||
|
this.content([ this.$container ]);
|
||||||
|
this.pointers.on('move', $pointer => {
|
||||||
|
const [x, y] = [$pointer.move_x, $pointer.move_y];
|
||||||
|
this.$container.css({transform: `translate(${x}, ${y})`});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addSlide(id: string, $element: $Element) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
@ -37,10 +37,10 @@ export class $DetailPanel extends $Container {
|
|||||||
new $Property('favorites').name('Favorites').content(this.post.favcount$),
|
new $Property('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),
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
@ -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}` : ''}` }
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
105
src/structure/PostManager.ts
Normal file
105
src/structure/PostManager.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { $EventManager } from "elexis";
|
||||||
|
import { Post } from "./Post";
|
||||||
|
import { Booru } from "./Booru";
|
||||||
|
|
||||||
|
export class PostManager {
|
||||||
|
static managers = new Map<string | undefined, PostManager>();
|
||||||
|
orderMap = new Map<id, Post>();
|
||||||
|
cache = new Set<Post>();
|
||||||
|
limit = 100;
|
||||||
|
tags?: string;
|
||||||
|
finished = false;
|
||||||
|
events = new $EventManager<PostManagerEventMap>();
|
||||||
|
constructor(tags?: string) {
|
||||||
|
this.tags = tags;
|
||||||
|
PostManager.managers.set(this.tags, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get(tags: string | undefined) {
|
||||||
|
const manager = this.managers.get(tags) ?? new PostManager(tags);
|
||||||
|
this.managers.set(manager.tags, manager);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.orderMap.clear();
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPosts(direction: 'newer' | 'older'): Promise<Post[]> {
|
||||||
|
const tags = this.tags ? decodeURIComponent(this.tags).split('+') : undefined;
|
||||||
|
const generalTags: string[] = [];
|
||||||
|
const orderTags: string[] = [];
|
||||||
|
let limit: number = this.limit;
|
||||||
|
let posts: Post[] = [];
|
||||||
|
if (tags) for (const tag of tags) {
|
||||||
|
if (tag.startsWith('ordfav:')) orderTags.push(tag);
|
||||||
|
else if (tag.startsWith('order:')) orderTags.push(tag);
|
||||||
|
else if (tag.startsWith('limit:')) limit = Number(tag.split(':')[1]);
|
||||||
|
else generalTags.push(tag);
|
||||||
|
}
|
||||||
|
if (orderTags.length) {
|
||||||
|
if (orderTags.length > 1) {
|
||||||
|
this.events.fire('post_error', `Error: These query can't be used together [${orderTags}].`)
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const orderTag = orderTags[0];
|
||||||
|
if (orderTag.startsWith('ordfav:')) {
|
||||||
|
const username = orderTag.split(':')[1];
|
||||||
|
const match_tags = generalTags.length ? `&search[post_tags_match]=${generalTags.toString().replaceAll(',', '+')}` : '';
|
||||||
|
const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `&search[id]=>${this.orderKeyList.at(0)}` : `&search[id]=<${this.orderKeyList.at(-1)}` : undefined;
|
||||||
|
const favoritesDataList = await Booru.used.fetch<FavoritesData[]>(`/favorites.json?search[user_name]=${username}${beforeAfter ?? ''}${match_tags}&limit=${limit}`);
|
||||||
|
posts = await Post.fetchMultiple(Booru.used, {tags: `id:${favoritesDataList.map(data => data.post_id).toString()}`});
|
||||||
|
const newPostOrderMap = new Map();
|
||||||
|
for (const fav of favoritesDataList) {
|
||||||
|
const post = posts.find(post => post.id === fav.post_id);
|
||||||
|
if (!post) continue;
|
||||||
|
if (!post.file_url) continue;
|
||||||
|
newPostOrderMap.set(fav.id, post);
|
||||||
|
}
|
||||||
|
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap]);
|
||||||
|
this.events.fire('post_fetch', posts);
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderTag.startsWith('order:')) {
|
||||||
|
const page = this.orderKeyList.length ? direction === 'newer' ? 1 : (this.orderMap.size / limit) + 1 : undefined;
|
||||||
|
posts = await Post.fetchMultiple(Booru.used, {tags: this.tags}, limit, page);
|
||||||
|
const newPostOrderMap = new Map(posts.filter(post => post.file_url).map(post => [post.id, post]));
|
||||||
|
newPostOrderMap.forEach((post, id) => { if (this.orderMap.has(id)) newPostOrderMap.delete(id) });
|
||||||
|
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap])
|
||||||
|
this.events.fire('post_fetch', posts);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `a${this.orderKeyList.at(0)}` : `b${this.orderKeyList.at(-1)}` : undefined;
|
||||||
|
posts = await Post.fetchMultiple(Booru.used, {tags: this.tags}, limit, beforeAfter);
|
||||||
|
const newPostOrderMap = new Map(posts.filter(post => post.file_url).map(post => [post.id, post]));
|
||||||
|
this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!posts.length) {
|
||||||
|
this.finished = true;
|
||||||
|
if (!this.cache.size) this.events.fire('noPost');
|
||||||
|
else this.events.fire('endPost')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.events.fire('post_fetch', posts);
|
||||||
|
return posts
|
||||||
|
}
|
||||||
|
|
||||||
|
get orderKeyList() { return [...this.orderMap.keys()]}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostManagerEventMap {
|
||||||
|
startLoad: [];
|
||||||
|
noPost: [];
|
||||||
|
endPost: [];
|
||||||
|
post_error: [message: string];
|
||||||
|
post_fetch: [Post[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FavoritesData {
|
||||||
|
id: id;
|
||||||
|
post_id: id;
|
||||||
|
user_id: id;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import type { Booru } from "./Booru";
|
import { Booru } from "./Booru";
|
||||||
|
|
||||||
export interface TagOptions {}
|
export interface 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();
|
||||||
|
Loading…
Reference in New Issue
Block a user