fix: jumping scroll when open page.
new: new FileType 'webp'.
change: $PostGrid get posts limit change to 100 post per time.
new: sound icon come out beside on video duration if video has sound.
fix: image too big when new posts insert to $PostGrid, disable loading post's transition.
new: mouse hover/down or touch on $PostTile have scale transition.
change: $PostTile video detail color change to dark theme.
change: Post all $State property have a loading string.
new: Post.ready will return Promise when post data is fetched.
new: Post.hasSound, Post.previewURL
optimize: post_route get Post and fetching.
fix: post page video size will be clarify when rendered.
This commit is contained in:
defaultkavy 2024-10-06 15:07:48 +08:00
parent b5eb4811d6
commit 1a6d0e580f
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
13 changed files with 182 additions and 140 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

1
dist/assets/index-CbgOs0Kp.css vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View File

@ -7,8 +7,8 @@
<title>Danbooru Viewer v0.2.5</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-359h29Hw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BXPCnb25.css">
<script type="module" crossorigin src="/assets/index-C8Fnii2g.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CbgOs0Kp.css">
</head>
<body>
</body>

View File

@ -31,6 +31,7 @@ html {
}
}
body {
overflow-x: hidden;
background-color: var(--background-color);
color: var(--primary-color);
margin: 0;

View File

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

View File

@ -63,14 +63,14 @@ export class $PostGrid extends $Layout {
async updateNewest() {
const latestPost = this.sortedPosts.at(0);
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: latestPost ? `>${latestPost.id}` : undefined});
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: latestPost ? `>${latestPost.id}` : undefined}, 100);
this.addPost(posts);
return this;
}
async getPosts() {
const oldestPost = this.sortedPosts.at(-1);
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: oldestPost ? `<${oldestPost.id}` : undefined});
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: oldestPost ? `<${oldestPost.id}` : undefined}, 100);
this.addPost(posts);
return this;
}

View File

@ -25,14 +25,22 @@ export class $PostTile extends $Container {
clearInterval(timer);
this.durationUpdate();
})
this.content([
this.post.isVideo ? $('span').class('duration').content(this.duration$) : null,
this.class('loading').content([
// Video Detail
this.post.isVideo
? $('div').class('video-detail').content([
this.post.hasSound ? $('ion-icon').name('volume-medium-outline') : null,
$('span').class('duration').content(this.duration$)
]) : null,
// Tile
$('a').href(this.post.pathname).content(() => [
this.$video,
$('img').css({opacity: '0'}).width(this.post.image_width).height(this.post.image_height).src(this.post.preview_file_url).loading('lazy')
$('img').draggable(false).css({opacity: '0'}).width(this.post.image_width).height(this.post.image_height).src(this.post.previewURL).loading('lazy')
.on('mousedown', (e) => e.preventDefault())
.once('load', (e, $img) => {
if (!this.post.isVideo) $img.src(this.post.large_file_url);
if (!this.post.isVideo) $img.src(this.post.previewURL);
$img.animate({opacity: [0, 1]}, {duration: 300, fill: 'both'});
this.removeClass('loading')
})
])
.on('mouseenter', () => {
@ -43,12 +51,20 @@ export class $PostTile extends $Container {
.on('mouseleave', () => {
this.$video?.pause().currentTime(0).hide(true);
})
.on('touchstart', () => {
if (!this.$video?.isPlaying) {
this.$video?.src(this.post.large_file_url).hide(false).play().catch(err => undefined)
}
})
.on('touchend', () => {
this.$video?.pause().currentTime(0).hide(true);
})
])
}
durationUpdate() {
if (!this.$video) return;
const t = time(this.post.media_asset.duration * 1000 - this.$video.currentTime() * 1000)
this.duration$.set(`${t.hh}:${t.mm}:${t.ss}`)
this.duration$.set(Number(t.hh) > 0 ? `${t.hh}:${t.mm}:${t.ss}` : `${t.mm}:${t.ss}`)
}
}

View File

@ -2,33 +2,58 @@ post-tile {
display: block;
transition: 0.3s all ease;
position: relative;
transition: all 0.3s ease;
border-radius: 10px;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
user-select: none;
&[filetype="mp4"], &[filetype="webm"], &[filetype="zip"] {
span.duration {
background-color: #ffffff;
color: #000000;
@media (hover: hover) {
&:hover {
transform: scale(1.02);
z-index: 1;
box-shadow: 0 0 10px color-mix(in srgb, var(--background-color) 50%, transparent)
}
}
span.duration {
&.loading {
transition: none;
}
&:active {
transform: scale(0.95);
}
div.video-detail {
position: absolute;
background-color: #00000050;
background-color: var(--background-color-lighter);//color-mix(in srgb, var(--background-color-lighter) 80%, transparent);
color: var(--primary-color);
bottom: 0.3rem;
right: 0.3rem;
padding: 2px 4px;
border-radius: 4px;
font-size: 12px;
display: flex;
align-items: center;
gap: 0.2rem;
z-index: 2;
// text-shadow: 0 0 0.5em var(--background-color);
ion-icon {
font-size: 1.5em;
}
span.duration {
text-transform: uppercase;
z-index: 2;
}
}
img {
border-radius: 10px;
height: 100%;
width: 100%;
vertical-align: top;
background-color: var(--background-color);
}
video {
border-radius: 10px;
height: 100%;
width: 100%;
object-fit: cover;

2
src/index.d.ts vendored
View File

@ -11,7 +11,7 @@ type Source = 'http' | `https://${string}` | `*${string}*` | 'none'
type Ratio = `${numebr}:${numebr}` | `${number}/${number}` | number
type FileSize = `${number}${FileSizeUnit}`
type FileSizeUnit = 'b' | 'kb' | 'm'
type FileType = 'jpg' | 'png' | 'gif' | 'swf' | 'webm' | 'mp4' | 'zip'
type FileType = 'jpg' | 'png' | 'gif' | 'swf' | 'webm' | 'mp4' | 'zip' | 'webp'
type seconds = number;
type poolname = string;

View File

@ -5,69 +5,36 @@ import { ArtistCommentary } from "../../structure/Commentary";
import { Booru } from "../../structure/Booru";
export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => {
if (!Number(params.id)) return '404';
const post = new Post(Booru.used, +params.id);
const ele = {
$viewer: $('div').class('viewer'),
$tags: $('div').class('post-tags'),
$commentary: $('section').class('commentary')
}
load();
async function load() {
await post.fetch();
ele.$viewer.content([
post.isVideo
? $('video').src(post.file_ext === 'zip' ? post.large_file_url : post.file_url).controls(true)
: $('img').src(post.large_file_url)//.once('load', (e, $img) => { $img.src(post.file_url)})
])
loadTags();
loadCommentary();
async function loadTags() {
const tags = await post.fetchTags();
const [artist_tags, char_tags, gen_tags, meta_tags, copy_tags] = [
tags.filter(tag => tag.category === TagCategory.Artist),
tags.filter(tag => tag.category === TagCategory.Character),
tags.filter(tag => tag.category === TagCategory.General),
tags.filter(tag => tag.category === TagCategory.Meta),
tags.filter(tag => tag.category === TagCategory.Copyright),
]
ele.$tags.content([
tag_category('Artist', artist_tags),
tag_category('Character', char_tags),
tag_category('Copyright', copy_tags),
tag_category('Meta', meta_tags),
tag_category('General', gen_tags),
])
function tag_category(category: string, tags: Tag[]) {
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(tag.post_count$)
]))
])
] : null
}
}
async function loadCommentary() {
const commentary = (await ArtistCommentary.fetchMultiple(Booru.used, {post: {_id: post.id}})).at(0);
if (!commentary) return ele.$commentary.content('No commentary');
ele.$commentary.content([
commentary.original_title ? $('h3').content(commentary.original_title) : null,
$('pre').content(commentary.original_description)
])
}
}
if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
const post = Post.get(Booru.used, +params.id);
return [
ele.$viewer,
$('div').class('viewer').content(async () => {
await post.ready;
return post.isVideo
? $('video').height(post.image_height).width(post.image_width).src(post.file_ext === 'zip' ? post.large_file_url : post.file_url).controls(true).autoplay(true).loop(true)
: $('img').src(post.large_file_url)//.once('load', (e, $img) => { $img.src(post.file_url)})
}),
$('div').class('content').content([
$('h3').content(`Artist's Commentary`),
ele.$commentary.content('loading...')
$('section').class('commentary').content(async ($comentary) => {
const commentary = (await ArtistCommentary.fetchMultiple(Booru.used, {post: {_id: post.id}})).at(0);
return [
commentary ? [
commentary.original_title ? $('h3').content(commentary.original_title) : null,
$('pre').content(commentary.original_description)
] : 'No commentary'
]
})
]),
$('div').class('sidebar').content([
$('div').class('sidebar')
.self($sidebar => {
let scrollTop = 0;
addEventListener('scroll', () => { if ($sidebar.inDOM()) scrollTop = document.documentElement.scrollTop })
$route
.on('beforeShift', () => { if (innerWidth > 800) $sidebar.css({position: `absolute`, top: `${scrollTop}px`}) })
.on('afterShift', () => $sidebar.css({position: '', top: ''}))
})
.content([
$('section').class('post-info').content([
new $Property('id').name('Post').value(`#${params.id}`),
new $Property('uploader').name('Uploader').value(post.uploader$),
@ -79,8 +46,7 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
new $Property('favorites').name('Favorites').value(post.favorites$),
new $Property('score').name('Score').value(post.score$)
]),
$('button').content('Copy link')
.on('click', (e, $button) => {
$('button').content('Copy link').on('click', (e, $button) => {
e.preventDefault();
navigator.clipboard.writeText(`${Booru.used.origin}${location.pathname}`);
$button.content('Copied!');
@ -89,14 +55,36 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
}, 2000);
})
]),
ele.$tags.content('loading...')
]).self($sidebar => {
let scrollTop = 0;
addEventListener('scroll', () => { if ($sidebar.inDOM()) scrollTop = document.documentElement.scrollTop })
$route
.on('beforeShift', () => { if (innerWidth > 800) $sidebar.css({position: `absolute`, top: `${scrollTop}px`}) })
.on('afterShift', () => $sidebar.css({position: '', top: ''}))
$('div').class('post-tags').content(async $tags => {
const tags = await post.fetchTags();
const [artist_tags, char_tags, gen_tags, meta_tags, copy_tags] = [
tags.filter(tag => tag.category === TagCategory.Artist),
tags.filter(tag => tag.category === TagCategory.Character),
tags.filter(tag => tag.category === TagCategory.General),
tags.filter(tag => tag.category === TagCategory.Meta),
tags.filter(tag => tag.category === TagCategory.Copyright),
]
return [
$tag_category('Artist', artist_tags),
$tag_category('Character', char_tags),
$tag_category('Copyright', copy_tags),
$tag_category('Meta', meta_tags),
$tag_category('General', gen_tags),
]
function $tag_category(category: string, tags: Tag[]) {
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(tag.post_count$)
]))
])
] : null
}
})
])
]
})

View File

@ -3,25 +3,35 @@ import { Booru } from "./Booru";
import { Tag } from "./Tag";
import { User } from "./User";
import { dateFrom, digitalUnit } from "./Util";
const LOADING_STRING = '...'
export interface PostOptions {}
export interface Post extends PostData {}
export class Post extends $EventManager<{update: []}> {
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);
file_size$ = $.state('');
file_ext$ = $.state(this.file_ext);
dimension$ = $.state('');
uploader$ = $.state(LOADING_STRING);
approver$ = $.state(LOADING_STRING);
created_date$ = $.state(LOADING_STRING);
favorites$ = $.state(0);
score$ = $.state(0);
file_size$ = $.state(LOADING_STRING);
file_ext$ = $.state(LOADING_STRING);
dimension$ = $.state(LOADING_STRING);
createdDate = new Date(this.created_at);
ready?: Promise<this>;
booru: Booru;
constructor(booru: Booru, id: id) {
constructor(booru: Booru, id: id, data?: PostData) {
super();
this.booru = booru;
this.id = id;
booru.posts.set(this.id, this);
if (data) this.update(data);
else this.ready = this.fetch();
}
static get(booru: Booru, id: id) {
return booru.posts.get(id) ?? new Post(booru, id);
}
async fetch() {
@ -47,8 +57,7 @@ export class Post extends $EventManager<{update: []}> {
const req = await fetch(`${booru.origin}/posts.json?limit=${limit}&tags=${tagsQuery}&_method=get`);
const dataArray: PostData[] = await req.json();
const list = dataArray.map(data => {
const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id);
instance.update(data);
const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id, data);
booru.posts.set(instance.id, instance);
return instance;
});
@ -78,6 +87,7 @@ export class Post extends $EventManager<{update: []}> {
}
async fetchTags() {
await this.ready;
return await Tag.fetchMultiple(this.booru, {name: {_space: this.tag_string}});
}
@ -85,10 +95,12 @@ export class Post extends $EventManager<{update: []}> {
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 isVideo() { return this.file_ext === 'mp4' || this.file_ext === 'webm' || this.file_ext === 'zip' }
get hasSound() { return this.tag_string_meta.includes('sound') }
get tags() {
const tag_list = this.tag_string.split(' ');
return [...this.booru.tags.values()].filter(tag => tag_list.includes(tag.name))
}
get previewURL() { return this.media_asset.variants.find(variant => variant.file_ext === 'webp')?.url ?? this.large_file_url }
}
export interface PostData extends PostOptions {