new: Auto detect and add new post.
This commit is contained in:
defaultkavy 2024-05-19 02:00:55 +08:00
parent 1add8ce529
commit 6855e7eca8
13 changed files with 165 additions and 106 deletions

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
post{display:block}post[filetype=mp4] span.duration,post[filetype=webm] span.duration,post[filetype=zip] span.duration{background-color:#fff;color:#000}post span.duration{position:absolute;background-color:#00000050;bottom:.3rem;right:.3rem;padding:2px 4px;border-radius:4px;font-size:12px;text-transform:uppercase;z-index:2}post img{border-radius:10px;height:100%;width:100%}post video{border-radius:10px;height:100%;width:100%;object-fit:cover;position:absolute}#post section{background-color:#2f2f45;border-radius:20px;padding:20px}#post div.main{width:calc(100vw - 300px - 4rem);padding-right:10px;display:flex;flex-direction:column}@media (max-width: 800px){#post div.main{width:100%}}#post div.main::-webkit-scrollbar{background-color:#000;width:4px}#post div.main::-webkit-scrollbar-thumb{background-color:#aeaeec;border-radius:2px}#post div.main>h3{padding-left:1rem;margin-block:1rem}#post div.main div.viewer{height:calc(100vh - 2rem);display:flex;justify-content:center;align-items:center;background-color:#000;border-radius:20px;overflow:hidden}#post div.main div.viewer img{max-width:100%;max-height:100%}#post div.main div.viewer video{max-width:100%;max-height:100%}#post div.main section.commentary *{text-wrap:wrap;word-break:break-word}#post div.sidebar{position:fixed;top:1rem;right:1rem;display:flex;flex-direction:column;gap:.4rem;width:300px;overflow:scroll;overflow-x:hidden;height:calc(100vh - 2rem);border-radius:20px}@media (max-width: 800px){#post div.sidebar{position:relative;width:100%;overflow:visible;height:100%;padding-inline:1rem;padding-bottom:1rem}}#post div.sidebar::-webkit-scrollbar{background-color:#000;width:0px}#post div.sidebar::-webkit-scrollbar-thumb{background-color:#aeaeec}#post div.sidebar h3{padding-left:1rem;margin-block:.6rem}#post div.sidebar .post-info{background-color:#2f2f45;border-radius:20px;padding:20px;display:flex;flex-direction:column;gap:.4rem}#post div.sidebar div.property{display:flex;gap:.6rem;align-items:center}#post div.sidebar div.property div.property-values{display:flex;gap:.4rem}#post div.sidebar div.property div.property-values span.property-value{background-color:#525278;color:#aeaeec;padding:2px 4px;border-radius:4px}#post div.sidebar div.inline{display:flex;gap:1rem}#post div.sidebar div.post-tags{display:flex;flex-direction:column;gap:.2rem}#post div.sidebar div.post-tags div.tag{align-items:center}#post div.sidebar div.post-tags div.tag a.tag-name{word-break:break-word;color:#d1d1ee;text-decoration:none}#post div.sidebar div.post-tags div.tag span.tag-post-count{background-color:#525278;color:#aeaeec;padding:0 4px;border-radius:4px;font-size:12px;margin-left:.4rem}page#root .loader{text-align:center;padding-block:2rem}body{background-color:#1e1e2c;color:#d1d1ee;margin:0;font-family:Microsoft Yahei;font-size:14px}body::-webkit-scrollbar{background-color:#000;width:8px}body::-webkit-scrollbar-thumb{background-color:#aeaeec;border-radius:2px}app{display:block}app view{display:block}app view page{min-height:100%;padding:1rem;display:block}
post{display:block}post[filetype=mp4] span.duration,post[filetype=webm] span.duration,post[filetype=zip] span.duration{background-color:#fff;color:#000}post span.duration{position:absolute;background-color:#00000050;bottom:.3rem;right:.3rem;padding:2px 4px;border-radius:4px;font-size:12px;text-transform:uppercase;z-index:2}post img{border-radius:10px;height:100%;width:100%}post video{border-radius:10px;height:100%;width:100%;object-fit:cover;position:absolute}#post section{background-color:#2f2f45;border-radius:20px;padding:20px}#post div.main{width:calc(100vw - 300px - 4rem);padding-right:10px;display:flex;flex-direction:column}@media (max-width: 800px){#post div.main{width:100%}}#post div.main::-webkit-scrollbar{background-color:#000;width:4px}#post div.main::-webkit-scrollbar-thumb{background-color:#aeaeec;border-radius:2px}#post div.main>h3{padding-left:1rem;margin-block:1rem}#post div.main div.viewer{height:calc(100vh - 2rem);display:flex;justify-content:center;align-items:center;background-color:#000;border-radius:20px;overflow:hidden}#post div.main div.viewer img{max-width:100%;max-height:100%}#post div.main div.viewer video{max-width:100%;max-height:100%}#post div.main section.commentary *{text-wrap:wrap;word-break:break-word}#post div.sidebar{position:fixed;top:1rem;right:1rem;display:flex;flex-direction:column;gap:.4rem;width:300px;overflow:scroll;overflow-x:hidden;height:calc(100vh - 2rem);border-radius:20px}@media (max-width: 800px){#post div.sidebar{position:relative;width:100%;overflow:visible;height:100%;padding-inline:1rem;padding-bottom:1rem}}#post div.sidebar::-webkit-scrollbar{background-color:#000;width:0px}#post div.sidebar::-webkit-scrollbar-thumb{background-color:#aeaeec}#post div.sidebar h3{padding-left:1rem;margin-block:.6rem}#post div.sidebar .post-info{background-color:#2f2f45;border-radius:20px;padding:20px;display:flex;flex-direction:column;gap:.4rem}#post div.sidebar div.property{display:flex;gap:.6rem;align-items:center}#post div.sidebar div.property div.property-values{display:flex;gap:.4rem}#post div.sidebar div.property div.property-values span.property-value{background-color:#525278;color:#aeaeec;padding:2px 4px;border-radius:4px}#post div.sidebar div.inline{display:flex;gap:1rem}#post div.sidebar div.post-tags{display:flex;flex-direction:column;gap:.2rem}#post div.sidebar div.post-tags div.tag{align-items:center}#post div.sidebar div.post-tags div.tag a.tag-name{word-break:break-word;color:#d1d1ee;text-decoration:none}#post div.sidebar div.post-tags div.tag span.tag-post-count{background-color:#525278;color:#aeaeec;padding:0 4px;border-radius:4px;font-size:12px;margin-left:.4rem}page#root layout *{transition:all .3s ease}page#root .loader{text-align:center;padding-block:2rem}html{overflow-x:hidden}body{background-color:#1e1e2c;color:#d1d1ee;margin:0;font-family:Microsoft Yahei;font-size:14px}body::-webkit-scrollbar{background-color:#000;width:8px}body::-webkit-scrollbar-thumb{background-color:#aeaeec;border-radius:2px}app{display:block}app view{display:block}app view page{min-height:100%;padding:1rem;display:block}

1
dist/assets/index-DR9E_CfL.js vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View File

@ -5,8 +5,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.1</title>
<script type="module" crossorigin src="/assets/index-BIoKxl_Q.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cq-5lgfU.css">
<script type="module" crossorigin src="/assets/index-DR9E_CfL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BMUJRc5c.css">
</head>
<body>
</body>

View File

@ -1,8 +1,11 @@
@import '/src/component/PostGrid/$PostGrid';
@import '/src/component/PostTile/$PostTile';
@import '/src/route/posts/$post';
@import '/src/route/home/$home';
@import '/src/route/gallery/$gallery';
html {
overflow-x: hidden;
}
body {
background-color: #1e1e2c;
color: #d1d1ee;

View File

@ -3,7 +3,7 @@ import '@elexis/layout';
import '@elexis/router';
import { Booru } from './structure/Booru';
import { Router } from '@elexis/router';
import { home_route } from './route/home/$home';
import { home_route } from './route/gallery/$gallery';
import { posts_route } from './route/posts/$post';
export const booru = new Booru({

View File

@ -0,0 +1,84 @@
import { Route, Router } from "@elexis/router";
import { Post } from "../../structure/Post";
import { booru } from "../../main";
import { $PostGrid } from "../../component/PostGrid/$PostGrid";
import { $PostTile } from "../../component/PostTile/$PostTile";
const MAX_POST_LENGTH = 100;
export const home_route = new Route((path) => {
if (path === '/posts' || path === '/') return '/';
}, ({record}) => {
const $page = $('page').id('root');
async function load(tags: string) {
const posts = await Post.fetchMultiple(booru, tags.length ? {tags: tags} : undefined, MAX_POST_LENGTH)
const filtered_posts = posts.filter(post => post.file_url);
const $layout = $('layout').class('post-grid').type('waterfall').column(5).maxHeight(300).gap(10)
.content([
filtered_posts.map(post => new $PostTile(post))
]).on('resize', () => { resizeCheck() });
resizeCheck();
let FIRST_POST = posts.at(1)!;
let LAST_POST = posts.at(-1)!;
let SCROLL_LOADED = false;
let POST_ENDED = posts.length !== MAX_POST_LENGTH;
const $loader = $('div').class('loader').content( POST_ENDED ? `It's End` : 'Loading...');
window.addEventListener('scroll', async () => {
if (!$layout.inDOM()) return;
if (POST_ENDED) return;
if (SCROLL_LOADED) return;
if (document.documentElement.scrollTop < document.documentElement.scrollHeight - innerHeight * 3) return;
SCROLL_LOADED = true;
const posts = await Post.fetchMultiple(booru, tags.length ? {tags: tags, id: `..${LAST_POST.id - 1}`} : {id: `..${LAST_POST.id - 1}`}, MAX_POST_LENGTH)
$layout.insert(
posts.filter(post => post.file_url).map(post => new $PostTile(post))
).render();
if (posts.length !== MAX_POST_LENGTH) {
$loader.content(`It's End`);
POST_ENDED = true;
}
LAST_POST = posts.at(-1)!;
SCROLL_LOADED = false;
})
setInterval(async () => {
if (!$layout.inDOM()) return;
if (document.documentElement.scrollTop !== 0) return;
const posts = await Post.fetchMultiple(booru, tags.length ? {tags: tags, id: `${FIRST_POST.id + 2}..`} : {id: `${FIRST_POST.id + 2}..`}, MAX_POST_LENGTH)
const filtered_posts = posts.filter(post => post.file_url)
if (posts.length) FIRST_POST = posts.at(0)!;
if (filtered_posts.length) $layout.insert(filtered_posts.map(post => new $PostTile(post)), 0).render();
}, 10_000)
return {$layout, $loader}
function resizeCheck() {
if (innerWidth < 350) $layout.column(1);
else if (innerWidth < 700) $layout.column(2);
else {
const col = Math.round(innerWidth / 300)
$layout.column(col);
}
}
}
const gridManager = new Map<string, $PostGrid>();
record.on('open', async () => {
const tags = new URL(location.href).searchParams.get('tags') ?? '';
const $cacheGrid = gridManager.get(tags);
if ($cacheGrid) {
$page.content($cacheGrid);
$cacheGrid.render();
return;
} else {
$page.clear();
}
const {$layout, $loader} = await load(tags);
$page.content([
$layout,
$loader
]);
$layout.render();
gridManager.set(tags, $layout);
Router.recoveryScrollPosition();
})
return $page;
})

View File

@ -1,4 +1,10 @@
page#root {
layout {
* {
transition: all 0.3s ease;
}
}
.loader {
text-align: center;
padding-block: 2rem;

View File

@ -1,72 +0,0 @@
import { Route, Router } from "@elexis/router";
import { Post } from "../../structure/Post";
import { booru } from "../../main";
import { $PostGrid } from "../../component/PostGrid/$PostGrid";
import { $PostTile } from "../../component/PostTile/$PostTile";
export const home_route = new Route((path) => {
if (path === '/posts' || path === '/') return '/';
}, ({record}) => {
const $page = $('page').id('root');
async function load(tags: string) {
const posts = await Post.fetchMultiple(booru, tags.length ? {tags: tags} : undefined, 100)
const $grid = new $PostGrid().content([
posts.filter(post => post.file_url).map(post => new $PostTile(post))
]).on('resize', () => { resizeCheck() });
resizeCheck();
let last_post = posts.at(-1)!;
let loaded = false;
let ended = posts.length !== 100;
const $loader = $('div').class('loader').content( ended ? `It's End` : 'Loading...');
window.addEventListener('scroll', async () => {
if (!$grid.inDOM()) return;
if (ended) return;
if (loaded) return;
if (document.documentElement.scrollTop < document.documentElement.scrollHeight - innerHeight * 3) return;
loaded = true;
const posts = await Post.fetchMultiple(booru, tags.length ? {tags: tags, id: `..${last_post.id - 1}`} : {id: `..${last_post.id - 1}`}, 100)
$grid.insert(
posts.filter(post => post.file_url).map(post => new $PostTile(post))
).render();
if (posts.length !== 100) {
$loader.content(`It's End`);
ended = true;
}
last_post = posts.at(-1)!;
loaded = false;
})
return {$grid, $loader}
function resizeCheck() {
if (innerWidth < 350) $grid.column(1);
else if (innerWidth < 700) $grid.column(2);
else {
const col = Math.round(innerWidth / 300)
$grid.column(col);
}
}
}
const gridManager = new Map<string, $PostGrid>();
record.on('open', async () => {
const tags = new URL(location.href).searchParams.get('tags') ?? '';
const $cacheGrid = gridManager.get(tags);
if ($cacheGrid) {
$page.content($cacheGrid);
$cacheGrid.render();
return;
} else {
$page.clear();
}
const {$grid, $loader} = await load(tags);
$page.content([
$grid,
$loader
]);
$grid.render();
gridManager.set(tags, $grid);
Router.recoveryScrollPosition();
})
return $page;
})

View File

@ -14,8 +14,8 @@ export const posts_route = new Route('/posts/:id', ({params}) => {
$commentary: $('section').class('commentary')
}
const value = {
uploader$: $.state<string|number>('loading...'),
approver$: $.state<string|number>('loading...'),
uploader$: $.state('loading...'),
approver$: $.state('loading...'),
date$: $.state('loading...'),
size$: $.state('loading...'),
dimension$: $.state(`loading...`),
@ -39,6 +39,8 @@ export const posts_route = new Route('/posts/:id', ({params}) => {
value.score$.set(post.score$)
value.ext$.set(post.file_ext.toUpperCase())
loadTags();
loadCommentary();
async function loadTags() {
const tags = await Tag.fetchMultiple(booru, {name: {_space: post.tag_string}});
const [artist_tags, char_tags, gen_tags, meta_tags, copy_tags] = [
@ -57,19 +59,17 @@ export const posts_route = new Route('/posts/:id', ({params}) => {
])
function tag_category(category: string, tags: Tag[]) {
const INTL_number = new Intl.NumberFormat('en', {notation: 'compact'})
return tags.length ? [
$('h3').content(category),
$('section').content([
tags.map(tag => $('div').class('tag').content([
$('a').class('tag-name').content(tag.name).href(`/posts?tags=${tag.name}`),
$('span').class('tag-post-count').content(`${INTL_number.format(tag.post_count)}`)
$('span').class('tag-post-count').content(tag.post_count$)
]))
])
] : null
}
}
loadCommentary();
async function loadCommentary() {
const commentary = (await ArtistCommentary.fetchMultiple(booru, {post: {_id: post.id}})).at(0);
if (!commentary) return ele.$commentary.content('No commentary');
@ -99,13 +99,14 @@ export const posts_route = new Route('/posts/:id', ({params}) => {
new $Property('favorites').name('Favorites').value(value.favorites$),
new $Property('score').name('Score').value(value.score$)
]),
$('a').content('Copy link').href(`${booru.api}${location.pathname}`).on('click', (e, $a) => {
navigator.clipboard.writeText($a.href());
$a.content('Copied!');
setTimeout(() => {
$a.content('Copy link')
}, 2000);
})
$('a').content('Copy link').href(`${booru.api}${location.pathname}`)
.on('click', (e, $a) => {
navigator.clipboard.writeText($a.href());
$a.content('Copied!');
setTimeout(() => {
$a.content('Copy link')
}, 2000);
})
]),
ele.$tags.content('loading...')
])

View File

@ -7,8 +7,8 @@ export interface PostOptions {}
export interface Post extends PostData {}
export class Post {
static manager = new Map<id, Post>();
uploader$ = $.state(this.uploader?.name ?? this.uploader_id);
approver$ = $.state(this.approver?.name ?? this.approver_id ?? 'None');
uploader$ = $.state(this.uploader?.name$ ?? this.uploader_id?.toString());
approver$ = $.state(this.approver?.name$ ?? this.approver_id?.toString() ?? 'None');
created_date$ = $.state(``);
favorites$ = $.state(this.fav_count);
score$ = $.state(this.score);
@ -18,10 +18,11 @@ export class Post {
}
static async fetch(booru: Booru, id: id) {
const req = await fetch(`${booru.api}/posts/${id}.json`);
const post = new this(await req.json());
User.fetchMultiple(booru, {id: [post.uploader_id, post.approver_id].detype(null)}).then(() => post.update$());
return post;
const data = await fetch(`${booru.api}/posts/${id}.json`).then(async data => await data.json()) as PostData;
const instance = this.manager.get(data.id)?.update(data) ?? new this(data);
this.manager.set(instance.id, instance);
User.fetchMultiple(booru, {id: [instance.uploader_id, instance.approver_id].detype(null)}).then(() => instance.update$());
return instance;
}
static async fetchMultiple(booru: Booru, tags?: Partial<MetaTags> | string, limit = 20) {
@ -40,23 +41,30 @@ export class Post {
const req = await fetch(`${booru.api}/posts.json?limit=${limit}${tagsQuery}&_method=get`);
const dataArray: PostData[] = await req.json();
const list = dataArray.map(data => {
const instance = new Post(data);
const instance = this.manager.get(data.id)?.update(data) ?? new this(data);
this.manager.set(instance.id, instance);
return instance;
});
if (!list.length) return list;
const userIds = [...new Set(dataArray.map(data => [data.approver_id, data.uploader_id].detype(null)).flat())];
User.fetchMultiple(booru, {id: userIds}).then(() => list.forEach(post => post.update$()));
return list;
}
update$() {
this.uploader$.set(this.uploader?.name ?? this.uploader_id);
this.approver$.set(this.approver?.name ?? this.approver_id ?? 'None');
this.uploader$.set(this.uploader?.name$ ?? this.uploader_id.toString());
this.approver$.set(this.approver?.name$ ?? this.approver_id?.toString() ?? 'None');
this.created_date$.set(dateFrom(+new Date(this.created_at)));
this.favorites$.set(this.fav_count);
this.score$.set(this.score);
}
update(data: PostData) {
Object.assign(this, data);
this.update$();
return this;
}
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 }

View File

@ -1,17 +1,22 @@
import type { Booru } from "./Booru";
const INTL_number = new Intl.NumberFormat('en', {notation: 'compact'})
export interface TagOptions {}
export interface Tag extends TagData {}
export class Tag {
static manager = new Map<id, Tag>();
post_count$ = $.state(0, {format: (value) => `${INTL_number.format(value)}`});
name$ = $.state('');
constructor(data: TagData) {
Object.assign(this, data);
this.$update();
}
static async fetch(booru: Booru, id: id) {
const req = await fetch(`${booru.api}/tags/${id}.json`);
const post = new this(await req.json());
return post;
const data = await fetch(`${booru.api}/tags/${id}.json`).then(async data => await data.json()) as TagData;
const instance = this.manager.get(data.id)?.update(data) ?? new this(data);
this.manager.set(instance.id, instance);
return instance;
}
static async fetchMultiple(booru: Booru, search?: Partial<TagSearchParams>, limit = 1000) {
@ -30,12 +35,23 @@ export class Tag {
const req = await fetch(`${booru.api}/tags.json?limit=${limit}${searchQuery}`);
const dataArray: TagData[] = await req.json();
const list = dataArray.map(data => {
const instance = new this(data);
const instance = this.manager.get(data.id)?.update(data) ?? new this(data);
this.manager.set(instance.id, instance);
return instance;
});
return list;
}
update(data: TagData) {
Object.assign(this, data);
this.$update();
return this;
}
$update() {
this.post_count$.set(this.post_count);
this.name$.set(this.name);
}
}
export interface TagData {

View File

@ -4,14 +4,17 @@ export class UserOptions {}
export interface User extends UserOptions, UserData {}
export class User {
static manager = new Map<id, User>();
name$ = $.state('loding...');
constructor(data: UserData) {
Object.assign(this, data);
this.update$();
}
static async fetch(booru: Booru, id: id) {
const req = await fetch(`${booru.api}/posts/${id}.json`);
const post = new this(await req.json());
return post;
const data = await fetch(`${booru.api}/users/${id}.json`).then(async data => await data.json()) as UserData;
const instance = this.manager.get(data.id)?.update(data) ?? new this(data);
this.manager.set(instance.id, instance);
return instance;
}
static async fetchMultiple(booru: Booru, search?: Partial<UserSearchParam>, limit = 200) {
@ -36,6 +39,16 @@ export class User {
});
return list;
}
update(data: UserData) {
Object.assign(this, data);
this.update$();
return this;
}
update$() {
this.name$.set(this.name);
}
}
export interface UserData {