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" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<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 nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<script type="module" crossorigin src="/assets/index-BM2d6uNq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BW2KEYV0.css">
<script type="module" crossorigin src="/assets/index-8i15BK3r.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C3nfTvuP.css">
</head>
<body>
</body>

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<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">
<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>

View File

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

View File

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

View File

@ -2,15 +2,18 @@ import { $Layout, type $LayoutEventMap } from "@elexis/layout";
import { Booru } from "../../structure/Booru";
import { Post } from "../../structure/Post";
import { $PostTile } from "../PostTile/$PostTile";
import { User } from "../../structure/User";
interface $PostGridOptions {
tags?: string
}
export class $PostGrid extends $Layout<$PostGridEventMap> {
posts = new Set<Post>();
$posts = new Set<$PostTile>();
$posts = new Map<Post, $PostTile>();
orderMap = new Map<id, Post>();
tags?: string;
finished = false;
limit = 100;
constructor(options?: $PostGridOptions) {
super();
this.tags = options?.tags;
@ -20,7 +23,7 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
}
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', () => {
this.removeAll();
if (this.finished) {
@ -37,20 +40,12 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
protected async loader() {
if (!this.inDOM()) return setTimeout(() => this.loader(), 100);;
while (this.inDOM() && document.documentElement.scrollHeight <= innerHeight * 2) {
const posts = await this.getPosts();
if (!posts.length) {
this.finished = true;
if (!this.posts.size) this.events.fire('noPost');
return
}
const posts = await this.getPost('older');
if (!posts.length) return;
}
if (document.documentElement.scrollTop + innerHeight > document.documentElement.scrollHeight - innerHeight * 2) {
const posts = await this.getPosts();
if (!posts.length) {
this.finished = true;
this.events.fire('endPost');
return
}
const posts = await this.getPost('older');
if (!posts.length) return;
}
setTimeout(() => this.loader(), 100);
}
@ -66,10 +61,10 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
if (!post.file_url) continue;
if (this.posts.has(post)) continue;
const $post = new $PostTile(post);
this.$posts.add($post);
this.$posts.set(post, $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();
return this;
}
@ -81,26 +76,78 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
return this;
}
async updateNewest() {
const latestPost = this.sortedPosts.at(0);
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: latestPost ? `>${latestPost.id}` : undefined}, 100);
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);
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() {
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); }
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;
}

View File

@ -13,8 +13,8 @@ export class $Searchbar extends $Container {
super('searchbar');
this.build();
window.addEventListener('keyup', (e) => {
if (!this.inDOM() && e.key === '/') this.activate();
if (this.inDOM() && e.key === 'Escape') this.inactivate();
if (!this.inDOM() && e.key === '/') this.open();
if (this.inDOM() && e.key === 'Escape') this.close();
})
}
@ -36,11 +36,14 @@ export class $Searchbar extends $Container {
this.$selectionList
]),
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() {
this.hide(false);
this.$filter

View File

@ -47,13 +47,13 @@ $(document.body).content([
// Searchbar
$('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)}))
.on('click', () => $.open(location.href + '#search')),
.on('click', () => $searchbar.open()),
// Buttons
$('div').class('buttons').content([
// Search Icon
$('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)}))
.on('click', () => $.open(location.href + '#search')),
.on('click', () => $searchbar.open()),
// Switch Booru
$('ion-icon').class('switch').name('swap-horizontal').title('Switch Booru')
.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('logout', () => $account.hide(true))
})
.on('click', () => $.open(location.href + '#drawer'))
.on('click', () => $drawer.open())
])
]),
// 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
]
}),

View File

@ -2,6 +2,7 @@ import { $EventManager, type $EventMap } from "elexis";
import type { Post } from "./Post";
import type { Tag } from "./Tag";
import { ClientUser, type ClientUserData } from "./ClientUser";
import type { User } from "./User";
export interface BooruOptions {
origin: string;
@ -16,6 +17,7 @@ export class Booru {
user?: ClientUser;
posts = new Map<id, Post>();
tags = new Map<id, Tag>();
users = new Map<id, User>();
constructor(options: BooruOptions) {
Object.assign(this, options);
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;
}
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 = '';
if (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 [];
const list = dataArray.map(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 uploader() { return User.manager.get(this.uploader_id); }
get approver() { if (this.approver_id) return User.manager.get(this.approver_id); else return null }
get uploader() { return this.booru.users.get(this.uploader_id); }
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 isGif() { return this.file_ext === 'gif' }
get isUgoria() { return this.file_ext === 'zip' }

View File

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