v0.3.1
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:
parent
b5eb4811d6
commit
1a6d0e580f
1
dist/assets/index-359h29Hw.js
vendored
1
dist/assets/index-359h29Hw.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-BXPCnb25.css
vendored
1
dist/assets/index-BXPCnb25.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-C8Fnii2g.js
vendored
Normal file
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
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
4
dist/index.html
vendored
@ -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>
|
||||
|
@ -31,6 +31,7 @@ html {
|
||||
}
|
||||
}
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
background-color: var(--background-color);
|
||||
color: var(--primary-color);
|
||||
margin: 0;
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}`)
|
||||
}
|
||||
}
|
@ -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
2
src/index.d.ts
vendored
@ -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;
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user