fix: $PostGrid get/update post order not perform ascending.
remove: window title remove version string.
fix: $Searchbar open and close with key bugs.
change: User.manager move to instance Booru.users.
This commit is contained in:
defaultkavy 2024-10-09 16:15:21 +08:00
parent 73894739d0
commit 1aa20c00b2
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
13 changed files with 121 additions and 53 deletions

1
dist/assets/index-8i15BK3r.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

File diff suppressed because one or more lines are too long

6
dist/index.html vendored
View File

@ -4,11 +4,11 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> --> <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Danbooru Viewer v0.2.5</title> <title>Danbooru Viewer</title>
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script> <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script> <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<script type="module" crossorigin src="/assets/index-BM2d6uNq.js"></script> <script type="module" crossorigin src="/assets/index-8i15BK3r.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BW2KEYV0.css"> <link rel="stylesheet" crossorigin href="/assets/index-C3nfTvuP.css">
</head> </head>
<body> <body>
</body> </body>

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> --> <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Danbooru Viewer v0.2.5</title> <title>Danbooru Viewer</title>
<link rel="stylesheet" href="/index.scss"> <link rel="stylesheet" href="/index.scss">
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script> <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script> <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>

View File

@ -172,6 +172,11 @@ route#posts {
h2 { h2 {
margin: 0; margin: 0;
} }
div.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
} }
} }

View File

@ -2,7 +2,7 @@
"name": "danbooru-viewer", "name": "danbooru-viewer",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "0.5.0", "version": "0.5.1",
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"vite": "^5.4.8" "vite": "^5.4.8"

View File

@ -2,15 +2,18 @@ import { $Layout, type $LayoutEventMap } from "@elexis/layout";
import { Booru } from "../../structure/Booru"; 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 { User } from "../../structure/User";
interface $PostGridOptions { interface $PostGridOptions {
tags?: string tags?: string
} }
export class $PostGrid extends $Layout<$PostGridEventMap> { export class $PostGrid extends $Layout<$PostGridEventMap> {
posts = new Set<Post>(); posts = new Set<Post>();
$posts = new Set<$PostTile>(); $posts = new Map<Post, $PostTile>();
orderMap = new Map<id, Post>();
tags?: string; tags?: string;
finished = false; finished = false;
limit = 100;
constructor(options?: $PostGridOptions) { constructor(options?: $PostGridOptions) {
super(); super();
this.tags = options?.tags; this.tags = options?.tags;
@ -20,7 +23,7 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
} }
protected async init() { protected async init() {
setInterval(() => { if (this.inDOM() && document.documentElement.scrollTop === 0) this.updateNewest(); }, 10000); setInterval(() => { if (this.inDOM() && document.documentElement.scrollTop === 0) this.getPost('newer'); }, 10000);
Booru.events.on('set', () => { Booru.events.on('set', () => {
this.removeAll(); this.removeAll();
if (this.finished) { if (this.finished) {
@ -37,20 +40,12 @@ 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.getPosts(); const posts = await this.getPost('older');
if (!posts.length) { if (!posts.length) return;
this.finished = true;
if (!this.posts.size) this.events.fire('noPost');
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.getPosts(); const posts = await this.getPost('older');
if (!posts.length) { if (!posts.length) return;
this.finished = true;
this.events.fire('endPost');
return
}
} }
setTimeout(() => this.loader(), 100); setTimeout(() => this.loader(), 100);
} }
@ -66,10 +61,10 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
if (!post.file_url) continue; if (!post.file_url) continue;
if (this.posts.has(post)) continue; if (this.posts.has(post)) continue;
const $post = new $PostTile(post); const $post = new $PostTile(post);
this.$posts.add($post); this.$posts.set(post, $post);
this.posts.add(post); this.posts.add(post);
} }
const $posts = [...this.$posts.values()].sort((a, b) => +b.post.createdDate - +a.post.createdDate); const $posts = [...this.orderMap.values()].map(post => this.$posts.get(post));
this.content($posts).render(); this.content($posts).render();
return this; return this;
} }
@ -81,26 +76,78 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
return this; return this;
} }
async updateNewest() { async getPost(direction: 'newer' | 'older'): Promise<Post[]> {
const latestPost = this.sortedPosts.at(0); const tags = this.tags ? decodeURIComponent(this.tags).split('+') : undefined;
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: latestPost ? `>${latestPost.id}` : undefined}, 100); 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); this.addPost(posts);
return posts;
if (!posts.length) {
this.finished = true;
if (!this.posts.size) this.events.fire('noPost');
else this.events.fire('endPost')
}
return posts
} }
async getPosts() { get orderKeyList() { return [...this.orderMap.keys()]}
const oldestPost = this.sortedPosts.at(-1);
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: oldestPost ? `<${oldestPost.id}` : undefined}, 100);
this.addPost(posts);
return posts;
}
get sortedPosts() { return this.posts.array.sort((a, b) => +b.createdDate - +a.createdDate); }
} }
interface $PostGridEventMap extends $LayoutEventMap { interface $PostGridEventMap extends $LayoutEventMap {
startLoad: []; startLoad: [];
noPost: []; noPost: [];
endPost: []; endPost: [];
post_error: [message: string]
}
interface FavoritesData {
id: id;
post_id: id;
user_id: id;
} }

View File

@ -13,8 +13,8 @@ export class $Searchbar extends $Container {
super('searchbar'); super('searchbar');
this.build(); this.build();
window.addEventListener('keyup', (e) => { window.addEventListener('keyup', (e) => {
if (!this.inDOM() && e.key === '/') this.activate(); if (!this.inDOM() && e.key === '/') this.open();
if (this.inDOM() && e.key === 'Escape') this.inactivate(); if (this.inDOM() && e.key === 'Escape') this.close();
}) })
} }
@ -36,11 +36,14 @@ export class $Searchbar extends $Container {
this.$selectionList this.$selectionList
]), ]),
this.$filter.on('click', () => { this.$filter.on('click', () => {
if (location.hash === '#search') $.back(); if (location.hash === '#search') this.close();
}) })
]) ])
} }
open() { $.open(location.href + '#search'); return this; }
close() { $.back(); return this; }
activate() { activate() {
this.hide(false); this.hide(false);
this.$filter this.$filter

View File

@ -47,13 +47,13 @@ $(document.body).content([
// Searchbar // Searchbar
$('div').class('searchbar').content(['Search in ', Booru.name$]) $('div').class('searchbar').content(['Search in ', Booru.name$])
.self($self => $Router.events.on('stateChange', ({beforeURL, afterURL}) => {if (beforeURL.hash === '#search') $self.hide(false); if (afterURL.hash === '#search') $self.hide(true)})) .self($self => $Router.events.on('stateChange', ({beforeURL, afterURL}) => {if (beforeURL.hash === '#search') $self.hide(false); if (afterURL.hash === '#search') $self.hide(true)}))
.on('click', () => $.open(location.href + '#search')), .on('click', () => $searchbar.open()),
// Buttons // Buttons
$('div').class('buttons').content([ $('div').class('buttons').content([
// Search Icon // Search Icon
$('ion-icon').class('search').name('search-outline').title('Search') $('ion-icon').class('search').name('search-outline').title('Search')
.self($self => $Router.events.on('stateChange', ({beforeURL, afterURL}) => {if (beforeURL.hash === '#search') $self.hide(false); if (afterURL.hash === '#search') $self.hide(true)})) .self($self => $Router.events.on('stateChange', ({beforeURL, afterURL}) => {if (beforeURL.hash === '#search') $self.hide(false); if (afterURL.hash === '#search') $self.hide(true)}))
.on('click', () => $.open(location.href + '#search')), .on('click', () => $searchbar.open()),
// Switch Booru // Switch Booru
$('ion-icon').class('switch').name('swap-horizontal').title('Switch Booru') $('ion-icon').class('switch').name('swap-horizontal').title('Switch Booru')
.on('click', () => { .on('click', () => {
@ -74,7 +74,7 @@ $(document.body).content([
.on('login', user => { $account.content(user.name$.convert(value => value.at(0)?.toUpperCase() ?? '')).hide(false); }) .on('login', user => { $account.content(user.name$.convert(value => value.at(0)?.toUpperCase() ?? '')).hide(false); })
.on('logout', () => $account.hide(true)) .on('logout', () => $account.hide(true))
}) })
.on('click', () => $.open(location.href + '#drawer')) .on('click', () => $drawer.open())
]) ])
]), ]),
// Searchbar // Searchbar
@ -97,7 +97,12 @@ $(document.body).content([
}) })
}) })
]), ]),
$('div').class('no-post').content('No Posts').hide(true).self($div => $postGrid.on('noPost', () => $div.hide(false)).on('startLoad', () => $div.hide(true))), $('div').class('no-post').hide(true).self($div => {
$div.on('startLoad', () => $div.hide(true))
$postGrid
.on('noPost', () => $div.hide(false).content('No Posts'))
.on('post_error', message => $div.hide(false).content(message))
}),
$postGrid $postGrid
] ]
}), }),

View File

@ -2,6 +2,7 @@ import { $EventManager, type $EventMap } from "elexis";
import type { Post } from "./Post"; import type { Post } from "./Post";
import type { Tag } from "./Tag"; import type { Tag } from "./Tag";
import { ClientUser, type ClientUserData } from "./ClientUser"; import { ClientUser, type ClientUserData } from "./ClientUser";
import type { User } from "./User";
export interface BooruOptions { export interface BooruOptions {
origin: string; origin: string;
@ -16,6 +17,7 @@ export class Booru {
user?: ClientUser; user?: ClientUser;
posts = new Map<id, Post>(); posts = new Map<id, Post>();
tags = new Map<id, Tag>(); tags = new Map<id, Tag>();
users = new Map<id, User>();
constructor(options: BooruOptions) { constructor(options: BooruOptions) {
Object.assign(this, options); Object.assign(this, options);
if (this.origin.endsWith('/')) this.origin = this.origin.slice(0, -1); if (this.origin.endsWith('/')) this.origin = this.origin.slice(0, -1);

View File

@ -45,7 +45,7 @@ export class Post extends $EventManager<{update: []}> {
return this; return this;
} }
static async fetchMultiple(booru: Booru, tags?: Partial<MetaTags> | string, limit = 20) { static async fetchMultiple(booru: Booru, tags?: Partial<MetaTags> | string, limit = 20, page?: string | number) {
let tagsQuery = ''; let tagsQuery = '';
if (tags) { if (tags) {
if (typeof tags === 'string') tagsQuery = tags; if (typeof tags === 'string') tagsQuery = tags;
@ -58,7 +58,7 @@ export class Post extends $EventManager<{update: []}> {
} }
} }
} }
const dataArray = await booru.fetch<PostData[]>(`/posts.json?limit=${limit}&tags=${tagsQuery}&_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 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);
@ -100,8 +100,8 @@ export class Post extends $EventManager<{update: []}> {
} }
get pathname() { return `/posts/${this.id}` } get pathname() { return `/posts/${this.id}` }
get uploader() { return User.manager.get(this.uploader_id); } get uploader() { return this.booru.users.get(this.uploader_id); }
get approver() { if (this.approver_id) return User.manager.get(this.approver_id); else return null } get approver() { if (this.approver_id) return this.booru.users.get(this.approver_id); else return null }
get isVideo() { return this.file_ext === 'mp4' || this.file_ext === 'webm' || this.file_ext === 'zip' } get isVideo() { return this.file_ext === 'mp4' || this.file_ext === 'webm' || this.file_ext === 'zip' }
get isGif() { return this.file_ext === 'gif' } get isGif() { return this.file_ext === 'gif' }
get isUgoria() { return this.file_ext === 'zip' } get isUgoria() { return this.file_ext === 'zip' }

View File

@ -3,7 +3,6 @@ import type { Booru } from "./Booru";
export class UserOptions {} export class UserOptions {}
export interface User extends UserOptions, UserData {} export interface User extends UserOptions, UserData {}
export class User { export class User {
static manager = new Map<id, User>();
name$ = $.state('...'); name$ = $.state('...');
post_upload_count$ = $.state(0); post_upload_count$ = $.state(0);
level$ = $.state(10); level$ = $.state(10);
@ -15,10 +14,17 @@ export class User {
if (update$) this.update$(); if (update$) this.update$();
} }
static async fetch(booru: Booru, id: id) { static async fetch(booru: Booru, id: username): Promise<User>;
const data = await booru.fetch<UserData>(`/users/${id}.json`); static async fetch(booru: Booru, id: id): Promise<User>;
const instance = this.manager.get(data.id)?.update(data) ?? new this(booru, data); static async fetch(booru: Booru, id: id | username) {
this.manager.set(instance.id, instance); let data: UserData;
if (typeof id === 'string') {
const res = (await booru.fetch<UserData[]>(`/users.json?search[name]=${id}`)).at(0);
if (!res) throw 'User Not Found';
return data = res;
} else data = await booru.fetch<UserData>(`/users/${id}.json`);
const instance = booru.users.get(data.id)?.update(data) ?? new this(booru, data);
booru.users.set(instance.id, instance);
return instance; return instance;
} }
@ -38,7 +44,7 @@ export class User {
const dataArray = await booru.fetch<UserData[]>(`/users.json?limit=${limit}${searchQuery}`); const dataArray = await booru.fetch<UserData[]>(`/users.json?limit=${limit}${searchQuery}`);
const list = dataArray.map(data => { const list = dataArray.map(data => {
const instance = new this(booru, data); const instance = new this(booru, data);
this.manager.set(instance.id, instance); booru.users.set(instance.id, instance);
return instance; return instance;
}); });
return list; return list;