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> <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 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-359h29Hw.js"></script> <script type="module" crossorigin src="/assets/index-C8Fnii2g.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BXPCnb25.css"> <link rel="stylesheet" crossorigin href="/assets/index-CbgOs0Kp.css">
</head> </head>
<body> <body>
</body> </body>

View File

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

View File

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

View File

@ -63,14 +63,14 @@ export class $PostGrid extends $Layout {
async updateNewest() { async updateNewest() {
const latestPost = this.sortedPosts.at(0); 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); this.addPost(posts);
return this; return this;
} }
async getPosts() { async getPosts() {
const oldestPost = this.sortedPosts.at(-1); 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); this.addPost(posts);
return this; return this;
} }

View File

@ -25,30 +25,46 @@ export class $PostTile extends $Container {
clearInterval(timer); clearInterval(timer);
this.durationUpdate(); this.durationUpdate();
}) })
this.content([ this.class('loading').content([
this.post.isVideo ? $('span').class('duration').content(this.duration$) : null, // 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(() => [ $('a').href(this.post.pathname).content(() => [
this.$video, 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) => { .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'}); $img.animate({opacity: [0, 1]}, {duration: 300, fill: 'both'});
this.removeClass('loading')
}) })
]) ])
.on('mouseenter', () => { .on('mouseenter', () => {
if (!this.$video?.isPlaying) { if (!this.$video?.isPlaying) {
this.$video?.src(this.post.large_file_url).hide(false).play().catch(err => undefined) this.$video?.src(this.post.large_file_url).hide(false).play().catch(err => undefined)
} }
}) })
.on('mouseleave', () => { .on('mouseleave', () => {
this.$video?.pause().currentTime(0).hide(true); 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() { durationUpdate() {
if (!this.$video) return; if (!this.$video) return;
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(`${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,37 +2,62 @@ post-tile {
display: block; display: block;
transition: 0.3s all ease; transition: 0.3s all ease;
position: relative; position: relative;
transition: all 0.3s ease;
border-radius: 10px;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
user-select: none;
@media (hover: hover) {
&[filetype="mp4"], &[filetype="webm"], &[filetype="zip"] { &:hover {
span.duration { transform: scale(1.02);
background-color: #ffffff; z-index: 1;
color: #000000; box-shadow: 0 0 10px color-mix(in srgb, var(--background-color) 50%, transparent)
}
} }
}
&.loading {
transition: none;
}
&:active {
transform: scale(0.95);
}
div.video-detail {
position: absolute;
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 { span.duration {
position: absolute;
background-color: #00000050;
bottom: 0.3rem;
right: 0.3rem;
padding: 2px 4px;
border-radius: 4px;
font-size: 12px;
text-transform: uppercase; text-transform: uppercase;
z-index: 2; z-index: 2;
} }
img { }
border-radius: 10px; img {
height: 100%; height: 100%;
width: 100%; width: 100%;
vertical-align: top; vertical-align: top;
} background-color: var(--background-color);
video { }
border-radius: 10px; video {
height: 100%; height: 100%;
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
} }
} }

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 Ratio = `${numebr}:${numebr}` | `${number}/${number}` | number
type FileSize = `${number}${FileSizeUnit}` type FileSize = `${number}${FileSizeUnit}`
type FileSizeUnit = 'b' | 'kb' | 'm' 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 seconds = number;
type poolname = string; type poolname = string;

View File

@ -5,82 +5,48 @@ import { ArtistCommentary } from "../../structure/Commentary";
import { Booru } from "../../structure/Booru"; import { Booru } from "../../structure/Booru";
export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => { export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => {
if (!Number(params.id)) return '404'; if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
const post = new Post(Booru.used, +params.id); const post = Post.get(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)
])
}
}
return [ 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([ $('div').class('content').content([
$('h3').content(`Artist's Commentary`), $('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')
$('section').class('post-info').content([ .self($sidebar => {
new $Property('id').name('Post').value(`#${params.id}`), let scrollTop = 0;
new $Property('uploader').name('Uploader').value(post.uploader$), addEventListener('scroll', () => { if ($sidebar.inDOM()) scrollTop = document.documentElement.scrollTop })
new $Property('approver').name('Approver').value(post.approver$), $route
new $Property('date').name('Date').value(post.created_date$), .on('beforeShift', () => { if (innerWidth > 800) $sidebar.css({position: `absolute`, top: `${scrollTop}px`}) })
new $Property('size').name('Size').value([post.file_size$, post.dimension$]), .on('afterShift', () => $sidebar.css({position: '', top: ''}))
new $Property('file').name('File Type').value(post.file_ext$), })
$('div').class('inline').content([ .content([
new $Property('favorites').name('Favorites').value(post.favorites$), $('section').class('post-info').content([
new $Property('score').name('Score').value(post.score$) new $Property('id').name('Post').value(`#${params.id}`),
]), new $Property('uploader').name('Uploader').value(post.uploader$),
$('button').content('Copy link') new $Property('approver').name('Approver').value(post.approver$),
.on('click', (e, $button) => { new $Property('date').name('Date').value(post.created_date$),
new $Property('size').name('Size').value([post.file_size$, post.dimension$]),
new $Property('file').name('File Type').value(post.file_ext$),
$('div').class('inline').content([
new $Property('favorites').name('Favorites').value(post.favorites$),
new $Property('score').name('Score').value(post.score$)
]),
$('button').content('Copy link').on('click', (e, $button) => {
e.preventDefault(); e.preventDefault();
navigator.clipboard.writeText(`${Booru.used.origin}${location.pathname}`); navigator.clipboard.writeText(`${Booru.used.origin}${location.pathname}`);
$button.content('Copied!'); $button.content('Copied!');
@ -88,15 +54,37 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
$button.content('Copy link') $button.content('Copy link')
}, 2000); }, 2000);
}) })
]), ]),
ele.$tags.content('loading...') $('div').class('post-tags').content(async $tags => {
]).self($sidebar => { const tags = await post.fetchTags();
let scrollTop = 0; const [artist_tags, char_tags, gen_tags, meta_tags, copy_tags] = [
addEventListener('scroll', () => { if ($sidebar.inDOM()) scrollTop = document.documentElement.scrollTop }) tags.filter(tag => tag.category === TagCategory.Artist),
$route tags.filter(tag => tag.category === TagCategory.Character),
.on('beforeShift', () => { if (innerWidth > 800) $sidebar.css({position: `absolute`, top: `${scrollTop}px`}) }) tags.filter(tag => tag.category === TagCategory.General),
.on('afterShift', () => $sidebar.css({position: '', top: ''})) 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 { Tag } from "./Tag";
import { User } from "./User"; import { User } from "./User";
import { dateFrom, digitalUnit } from "./Util"; import { dateFrom, digitalUnit } from "./Util";
const LOADING_STRING = '...'
export interface PostOptions {} export interface PostOptions {}
export interface Post extends PostData {} export interface Post extends PostData {}
export class Post extends $EventManager<{update: []}> { export class Post extends $EventManager<{update: []}> {
uploader$ = $.state(this.uploader?.name$ ?? this.uploader_id?.toString()); uploader$ = $.state(LOADING_STRING);
approver$ = $.state(this.approver?.name$ ?? this.approver_id?.toString() ?? 'None'); approver$ = $.state(LOADING_STRING);
created_date$ = $.state(``); created_date$ = $.state(LOADING_STRING);
favorites$ = $.state(this.fav_count); favorites$ = $.state(0);
score$ = $.state(this.score); score$ = $.state(0);
file_size$ = $.state(''); file_size$ = $.state(LOADING_STRING);
file_ext$ = $.state(this.file_ext); file_ext$ = $.state(LOADING_STRING);
dimension$ = $.state(''); dimension$ = $.state(LOADING_STRING);
createdDate = new Date(this.created_at); createdDate = new Date(this.created_at);
ready?: Promise<this>;
booru: Booru; booru: Booru;
constructor(booru: Booru, id: id) { constructor(booru: Booru, id: id, data?: PostData) {
super(); super();
this.booru = booru; this.booru = booru;
this.id = id; this.id = id;
booru.posts.set(this.id, this); 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() { 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 req = await fetch(`${booru.origin}/posts.json?limit=${limit}&tags=${tagsQuery}&_method=get`);
const dataArray: PostData[] = await req.json(); const dataArray: PostData[] = await req.json();
const list = dataArray.map(data => { const list = dataArray.map(data => {
const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id); const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id, data);
instance.update(data);
booru.posts.set(instance.id, instance); booru.posts.set(instance.id, instance);
return instance; return instance;
}); });
@ -78,6 +87,7 @@ export class Post extends $EventManager<{update: []}> {
} }
async fetchTags() { async fetchTags() {
await this.ready;
return await Tag.fetchMultiple(this.booru, {name: {_space: this.tag_string}}); 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 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 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 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() { get tags() {
const tag_list = this.tag_string.split(' '); const tag_list = this.tag_string.split(' ');
return [...this.booru.tags.values()].filter(tag => tag_list.includes(tag.name)) 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 { export interface PostData extends PostOptions {