- new - $DetailPanel on posts gallery page.
- new - hotkeys support:
  - toggle detail panel.
  - video play pause.
- new - LocalSettings
This commit is contained in:
defaultkavy 2024-10-18 16:28:32 +08:00
parent d6be4c0c62
commit 1913e010b6
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
18 changed files with 457 additions and 276 deletions

File diff suppressed because one or more lines are too long

1
dist/assets/index-BgVVzy-z.css 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

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

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View File

@ -16,8 +16,8 @@
gtag('config', 'G-59HBGP98WR');
</script>
<script type="module" crossorigin src="/assets/index-BMZsSbMp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ByWqsQtl.css">
<script type="module" crossorigin src="/assets/index-C_KR8nb7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BgVVzy-z.css">
</head>
<body>
</body>

View File

@ -5,6 +5,7 @@
@import '/src/component/IconButton/$IconButton';
@import '/src/component/IonIcon/$IonIcon';
@import '/src/component/Drawer/$Drawer';
@import '/src/component/DetailPanel/$DetailPanel';
// routes
@import '/src/route/post/$post_route';
@import '/src/route/login/$login_route';
@ -154,8 +155,14 @@ nav {
div.searchbar {
display: none;
}
div.buttons ion-icon.search {
display: inline-block;
div.buttons ion-icon {
&.search {
display: inline-block;
}
&.detail-panel {
display: none;
}
}
}
}
@ -173,7 +180,7 @@ router {
}
route#posts {
header {
margin-bottom: 1rem;
h2 {
@ -185,6 +192,25 @@ route#posts {
gap: 0.5rem;
}
}
.post-grid.detail-panel-enabled {
width: calc(100vw - 300px - 4rem);
@media (max-width: 800px) {
width: 100%;
}
}
@media (max-width: 800px) {
detail-panel {
display: none;
}
}
}
section {
background-color: #2f2f45;
border-radius: var(--border-radius-large);
padding: 20px;
}
button {

View File

@ -2,7 +2,7 @@
"name": "danbooru-viewer",
"module": "index.ts",
"type": "module",
"version": "0.9.0",
"version": "0.10.0",
"scripts": {
"dev": "bun x vite",
"build": "bun x vite build",

View File

@ -0,0 +1,162 @@
import { $Container, type $ContainerContentType } from "elexis";
import type { Post } from "../../structure/Post";
import { Booru } from "../../structure/Booru";
import { Tag, TagCategory } from "../../structure/Tag";
import { numberFormat } from "../../structure/Util";
import type { $IonIcon } from "../IonIcon/$IonIcon";
import type { $Route } from "@elexis/router";
export class $DetailPanel extends $Container {
post: Post | null = null;
options: $DetailPanelOptions;
constructor(options?: $DetailPanelOptions) {
super('detail-panel');
this.options = {
preview: options?.preview ?? false,
tagsType: options?.tagsType ?? 'detail'
};
this.build();
}
private build() {
if (this.post) {
this.content([
this.options.preview ? $('div').class('preview').content([
$('img').src(this.post.previewURL)
]) : null,
$('div').class('detail').content([
$('section').class('post-info').content([
new $Property('id').name('Post').content(`#${this.post.id}`),
new $Property('uploader').name('Uploader').content(this.post.uploader$),
new $Property('approver').name('Approver').content(this.post.approver$),
new $Property('date').name('Date').content(this.post.created_date$),
new $Property('size').name('Size').content([this.post.file_size$, this.post.dimension$]),
new $Property('file-type').name('File Type').content(this.post.file_ext$),
$('div').class('inline').content([
new $Property('favorites').name('Favorites').content(this.post.favcount$),
new $Property('score').name('Score').content(this.post.score$)
]),
new $Property('file-url').name('File').content([
$('a').href(this.post.file_url$).content(this.post.file_url$.convert((value) => value.replace('https://', ''))).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.file_url))
]),
new $Property('source-url').name('Source').content([
$('a').href(this.post.source$).content(this.post.source$.convert((value) => value.replace('https://', ''))).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.source))
]),
new $Property('booru-url').name(Booru.name$).content([
$('a').href(this.post.booruUrl$).content(this.post.booruUrl$.convert((value) => value.replace('https://', ''))).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.booruUrl))
]),
new $Property('webm-url').name('Webm').hide(true).self(async ($property) => {
await this.post!.ready;
if (this.post!.isUgoria) $property.content($('a').href(this.post!.webm_url$).content(this.post!.webm_url$.convert((value) => value.replace('https://', ''))).target('_blank')).hide(false);
}),
]),
$('div').class('post-tags').content(async $tags => {
if (this.options.tagsType === 'detail') {
const tags = await this.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),
]
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$.convert(numberFormat))
]))
])
] : null
}
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),
]
} else {
function $tag_category(category: string, tags: string[]) {
return tags.at(0)?.length ? [
$('h3').content(category),
$('section').class('tag-name-only').content([
tags.map(tag => $('a').class('tag').content(tag).href(`/posts?tags=${tag}`)),
])
] : null
}
return [
$tag_category('Artist', this.post!.tag_string_artist.split(' ')),
$tag_category('Character', this.post!.tag_string_character.split(' ')),
$tag_category('Copyright', this.post!.tag_string_copyright.split(' ')),
$tag_category('Meta', this.post!.tag_string_meta.split(' ')),
$tag_category('General', this.post!.tag_string_general.split(' ')),
]
}
})
])
])
} else {
this.content($('span').class('no-content').content('No Selected'))
}
}
update(post: Post | null) {
this.post = post;
this.build();
return this;
}
private copyButtonHandler($ion: $IonIcon, text: string) {
$ion.name('checkmark');
navigator.clipboard.writeText(text);
setTimeout(() => $ion.name('clipboard'), 3000);
}
position($route: $Route<any>) {
let scrollTop = 0;
addEventListener('scroll', () => { if (this.inDOM()) scrollTop = document.documentElement.scrollTop }, {passive: true})
$route
.on('beforeShift', () => { if (innerWidth > 800) this.css({position: `absolute`, top: `calc(${scrollTop}px + var(--nav-height) + var(--padding))`}) })
.on('afterShift', () => this.css({position: '', top: ''}))
return this;
}
}
export interface $DetailPanelOptions {
preview?: boolean;
tagsType?: 'detail' | 'name_only';
}
class $Property extends $Container {
$name = $('span').class('property-name')
$values = $('div').class('property-values')
constructor(id: string) {
super('div');
this.staticClass('property').attribute('property-id', id);
super.content([
this.$name,
this.$values.hide(true)
])
}
name(content: $ContainerContentType) {
this.$name.content(content);
return this;
}
content(content: OrMatrix<$ContainerContentType>) {
this.$values.hide(false);
const list = $.orArrayResolve(content);
this.$values.content(list.map($item => $('span').staticClass('property-value').content($item)));
return this;
}
}

View File

@ -0,0 +1,162 @@
detail-panel {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
--padding: 1rem;
position: fixed;
top: calc(var(--nav-height) + var(--padding));
right: var(--padding);
width: 300px;
height: calc(100dvh - 2rem - var(--nav-height));
// transition: all 0.3s ease;
background-color: var(--secondary-color-1);
@media (max-width: 800px) {
position: static;
width: 100%;
overflow: visible;
height: 100%;
padding: 1rem;
box-sizing: border-box;
}
span.no-content {
color: var(--secondary-color-3);
font-size: 1.6rem;
font-weight: 900;
}
div.preview {
overflow: hidden;
border-radius: var(--border-radius-large);
height: 300px;
width: 300px;
background-color: var(--secondary-color-0);
img {
height: 100%;
width: 100%;
object-fit: contain;
}
}
div.detail {
display: flex;
flex-direction: column;
gap: 0.4rem;
overflow: scroll;
overflow-x: hidden;
border-radius: var(--border-radius-large);
height: 100%;
width: 100%;
&::-webkit-scrollbar {
background-color: #000000;
width: 0px;
}
&::-webkit-scrollbar-thumb {
background-color: #aeaeec;
}
}
h3 {
padding-left: 1rem;
margin-block: 0.6rem;
}
.post-info {
background-color: #2f2f45;
border-radius: var(--border-radius-large);
padding: 20px;
display: flex;
flex-direction: column;
gap: 0.4rem;
.buttons {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
margin-top: 1rem;
}
}
div.property {
display: flex;
gap: 0.6rem;
align-items: center;
width: 100%;
span.property-name {
flex-shrink: 0;
}
div.property-values {
display: flex;
gap: 0.4rem;
width: 100%;
overflow: hidden;
span.property-value {
padding: 0.2rem 0.4rem;
background-color: var(--secondary-color-1);
color: var(--primary-color-dark);
border-radius: var(--border-radius-small);
justify-content: space-between;
flex-shrink: 1;
display: flex;
align-items: center;
overflow: hidden;
&:has(ion-icon) {
flex-shrink: 0;
}
* {
display: block;
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
flex-shrink: 1;
}
ion-icon {
font-size: 1rem;
padding: 4px;
box-sizing: border-box;
}
}
}
}
div.inline {
display: flex;
gap: 1rem;
}
div.post-tags {
display: flex;
flex-direction: column;
gap: 0.2rem;
div.tag {
align-items: center;
a.tag-name {
word-break: break-word;
text-decoration: none;
}
span.tag-post-count {
background-color: var(--secondary-color-3);
color: var(--secondary-color-8);
padding: 0px 4px;
border-radius: var(--border-radius-small);
font-size: 12px;
margin-left: 0.4rem;
}
}
section.tag-name-only {
display: flex;
flex-wrap: wrap;
column-gap: 0.5rem;
a.tag {
}
}
}
}

View File

@ -34,6 +34,9 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
}
})
this.on('resize', () => this.resize())
this.on('afterRender', () => {
this.$focus.currentLayer?.focus(this.$focus.currentLayer.currentFocus);
})
this.events.fire('startLoad');
this.loader();
this.$focus.layer(100).loop(false).scrollThreshold($.rem(2) + 60);
@ -44,11 +47,11 @@ export class $PostGrid extends $Layout<$PostGridEventMap> {
if (!this.inDOM()) return;
return true;
})
.keydown('Tab', e => {
e.preventDefault();
if (e.shiftKey) this.$focus.prev();
else this.$focus.next();
})
// .keydown('Tab', e => {
// e.preventDefault();
// if (e.shiftKey) this.$focus.prev();
// else this.$focus.next();
// })
.keydown(['w', 'W'], e => { e.preventDefault(); this.$focus.up(); })
.keydown(['s', 'S'], e => { e.preventDefault(); this.$focus.down(); })
.keydown(['d', 'D'], e => { e.preventDefault(); this.$focus.right(); })
@ -169,7 +172,8 @@ interface $PostGridEventMap extends $LayoutEventMap {
startLoad: [];
noPost: [];
endPost: [];
post_error: [message: string]
post_error: [message: string];
}
interface FavoritesData {

View File

@ -1,12 +1,19 @@
layout.post-grid {
margin-top: 0.4rem;
margin-block: 1rem;
a {
transition: 0.3s all ease;
}
&:has(post-tile[focus]) {
post-tile:not([focus]) {
opacity: 0.5;
a {
opacity: 0.5;
}
}
post-tile:hover {
opacity: 1;
a {
opacity: 1;
}
}
}
}

View File

@ -1,28 +1,26 @@
import { $Container, $Image, $State, $Video } from "elexis";
import type { Post } from "../../structure/Post";
import { time } from "../../structure/Util";
import { detailPanelEnable$ } from "../../main";
export class $PostTile extends $Container {
post: Post;
$video: $Video | null;
$img: $Image;
duration$ = $.state(``);
constructor(post: Post) {
super('post-tile');
this.post = post;
this.$video = this.post.isVideo ? $('video').width(this.post.image_width).height(this.post.image_height).disablePictureInPicture(true).loop(true).muted(true).hide(true).on('mousedown', (e) => e.preventDefault()) : null;
this.$img = $('img').draggable(false).css({opacity: '0'}).width(this.post.image_width).height(this.post.image_height).src(this.post.previewURL).loading('lazy');
this.attribute('filetype', this.post.file_ext);
this.durationUpdate();
this.build();
}
build() {
let timer: Timer
this.$video?.on('timeupdate', (e, $video) => {
this.durationUpdate();
})
this.$video?.on('pause', () => {
clearInterval(timer);
this.durationUpdate();
})
this.class('loading').content([
// Video Detail
this.post.isVideo
@ -37,22 +35,31 @@ export class $PostTile extends $Container {
$('span').content('GIF')
]) : null,
// Tile
$('a').href(this.post.pathname).content(() => [
$('a').href(this.post.pathname).preventDefault(detailPanelEnable$).content(() => [
this.$video,
$('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) => {
$img
.src(this.post.previewURL)
.on(['mouseenter', 'touchstart'], () => { if (this.post.isGif) { $img.src(this.post.large_file_url) } }, {passive: true})
.on(['mouseleave', 'touchend', 'touchcancel'], () => { if (this.post.isGif) { $img.src(this.post.previewURL) } }, {passive: true})
.animate({opacity: [0, 1]}, {duration: 300, fill: 'both'});
this.removeClass('loading');
this.$img.on('mousedown', (e) => e.preventDefault())
.once('load', (e, $img) => {
$img.animate({opacity: [0, 1]}, {duration: 300}, () => $img.css({opacity: ''}));
this.removeClass('loading');
})
])
.on(['mouseenter', 'touchstart'], () => { if (!this.$video?.isPlaying) { this.$video?.src(this.post.large_file_url).hide(false).play().catch(err => undefined) } }, {passive: true})
.on(['mouseleave', 'touchend', 'touchcancel'], () => { this.$video?.pause().currentTime(0).hide(true); }, {passive: true})
])
this.on(['focus', 'mouseenter', 'touchstart'], () => {
if (!this.$video?.isPlaying) {
this.$video?.src(this.post.large_file_url).hide(false).play().catch(err => undefined)
}
if (this.post.isGif) { this.$img.src(this.post.large_file_url) }
}, {passive: true} )
.on(['blur', 'mouseleave', 'touchend', 'touchcancel'], () => {
this.$video?.pause().currentTime(0).hide(true);
if (this.post.isGif) { this.$img.src(this.post.previewURL) }
}, {passive: true} )
.on('click', () => {
if (!detailPanelEnable$.value) return;
if (innerWidth <= 800) return $.open(this.post.pathname);
if ($(document.activeElement) === this) $.open(this.post.pathname);
else this.focus();
})
}
durationUpdate() {

View File

@ -8,6 +8,7 @@ post-tile {
-webkit-tap-highlight-color: transparent;
user-select: none;
outline: transparent solid 2px;
background-color: var(--secondary-color-1);
&[focus] {
outline: var(--secondary-color-9) solid 2px;

View File

@ -4,25 +4,29 @@ import '@elexis/router';
import { Booru } from './structure/Booru';
import { post_route } from './route/post/$post_route';
import { $PostGrid } from './component/PostGrid/$PostGrid';
import { $Router, $RouterNavigationDirection } from '@elexis/router';
import { $Route, $Router, $RouterAnchor, $RouterNavigationDirection } from '@elexis/router';
import { $Searchbar } from './component/Searchbar/$Searchbar';
import { $IonIcon } from './component/IonIcon/$IonIcon';
import { $IconButton } from './component/IconButton/$IconButton';
import { $login_route } from './route/login/$login_route';
import { $Drawer } from './component/Drawer/$Drawer';
import { $Input } from 'elexis/lib/node/$Input';
import { $DetailPanel } from './component/DetailPanel/$DetailPanel';
import { $PostTile } from './component/PostTile/$PostTile';
import { LocalSettings } from './structure/LocalSettings';
// declare elexis module
declare module 'elexis' {
export namespace $ {
export interface TagNameElementMap {
'ion-icon': typeof $IonIcon;
'icon-button': typeof $IconButton;
'a': typeof $RouterAnchor;
}
}
}
$.registerTagName('ion-icon', $IonIcon)
$.registerTagName('icon-button', $IconButton)
$.anchorHandler = ($a) => { $.open($a.href(), $a.target())}
$.registerTagName('a', $RouterAnchor)
// settings
export const [danbooru, safebooru]: Booru[] = [
new Booru({ origin: 'https://danbooru.donmai.us', name: 'Danbooru' }),
@ -32,6 +36,7 @@ export const [danbooru, safebooru]: Booru[] = [
Booru.set(Booru.manager.get(Booru.storageAPI ?? '') ?? danbooru);
const $searchbar = new $Searchbar().hide(true);
const $drawer = new $Drawer();
export const detailPanelEnable$ = $.state(LocalSettings.detailPanelEnabled ?? false).on('update', ({state$}) => LocalSettings.detailPanelEnabled = state$.value)
// render
$(document.body).content([
@ -55,6 +60,8 @@ $(document.body).content([
$('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', () => $searchbar.open()),
// Detail Panel Button
$('ion-icon').class('detail-panel').name('reader-outline').title('Toggle Detail Panel').on('click', () => detailPanelEnable$.set(!detailPanelEnable$.value)),
// Open Booru
$('a').content($('ion-icon').class('open').name('open-outline').title('Open in Original Site')).href(location.href.replace(location.origin, Booru.used.origin)).target('_blank'),
// Copy Button
@ -87,10 +94,13 @@ $(document.body).content([
// Base Router
$('router').base('/').map([
// Home Page
$('route').id('posts').path(['/', '/posts']).builder(() => new $PostGrid()),
$('route').id('posts').path(['/', '/posts']).builder(({$route, query}) => {
const { $postGrid, $detail } = $postsPageComponents($route, query);
return [ $postGrid, $detail ]
}),
// Posts Page
$('route').id('posts').path('/posts?tags').builder(({query}) => {
const $postGrid = new $PostGrid({tags: query.tags});
$('route').id('posts').path('/posts?tags').builder(({$route, query}) => {
const { $postGrid, $detail } = $postsPageComponents($route, query)
return [
$('header').content([
$('h2').content('Posts'),
@ -106,7 +116,8 @@ $(document.body).content([
.on('noPost', () => $div.hide(false).content('No Posts'))
.on('post_error', message => $div.hide(false).content(message))
}),
$postGrid
$postGrid,
$detail
]
}),
// Post Page
@ -173,10 +184,24 @@ componentState(undefined, new URL(location.href))
function componentState(beforeURL: URL | undefined, afterURL: URL) {
$searchbar.checkURL(beforeURL, afterURL); $drawer.checkURL(beforeURL, afterURL)
}
function $postsPageComponents($route: $Route, query: {tags?: string}) {
const $postGrid = new $PostGrid(query);
const $detail = new $DetailPanel({preview: true, tagsType: 'name_only'}).hide(detailPanelEnable$.convert(bool => !bool)).position($route);
detailPanelCheck();
detailPanelEnable$.on('update', detailPanelCheck)
function detailPanelCheck() { detailPanelEnable$.value ? $postGrid.addClass('detail-panel-enabled') : $postGrid.removeClass('detail-panel-enabled') }
$postGrid.$focus
.on('focus', ({$focused: $target}) => {if ($target instanceof $PostTile) $detail.update($target.post) })
.on('blur', () => $detail.update(null))
return { $postGrid, $detail };
}
$.keys($(window))
.if(e => {
if ($(e.target) instanceof $Input) return;
return true;
})
.keydown(['q', 'Q'], e => { e.preventDefault(); if ($Router.index !== 0) $.back(); })
.keydown(['e', 'E'], e => { e.preventDefault(); if ($Router.forwardIndex !== 0) $.forward(); })
.keydown(['e', 'E'], e => { e.preventDefault(); if ($Router.forwardIndex !== 0) $.forward(); })
.keydown('Tab', e => { e.preventDefault(); detailPanelEnable$.set(!detailPanelEnable$.value) })

View File

@ -1,17 +1,15 @@
import { Post } from "../../structure/Post";
import { $Container, type $ContainerContentType } from "elexis";
import { Tag, TagCategory } from "../../structure/Tag";
import { ArtistCommentary } from "../../structure/Commentary";
import { Booru } from "../../structure/Booru";
import type { $IonIcon } from "../../component/IonIcon/$IonIcon";
import { numberFormat } from "../../structure/Util";
import { ClientUser } from "../../structure/ClientUser";
import { $VideoController } from "../../component/VideoController/$VideoController";
import { $Input } from "elexis/lib/node/$Input";
import { $DetailPanel } from "../../component/DetailPanel/$DetailPanel";
export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => {
if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
const post = Post.get(Booru.used, +params.id);
const $video = $('video');
const events = $.events<{
viewerPanel_hide: [],
viewerPanel_show: [],
@ -29,9 +27,13 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite();
else post.createFavorite();
})
.keydown(' ', e => {
e.preventDefault();
if ($video.isPlaying) $video.pause();
else $video.play();
})
return [
$('div').class('viewer').content(async ($viewer) => {
const $video = $('video');
events.on('video_play_pause', () => { if ($video.isPlaying) $video.pause(); else $video.play() })
await post.ready;
$viewer
@ -95,103 +97,6 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
]
})
]),
$('div').class('sidebar')
.self($sidebar => {
let scrollTop = 0;
addEventListener('scroll', () => { if ($sidebar.inDOM()) scrollTop = document.documentElement.scrollTop }, {passive: true})
$route
.on('beforeShift', () => { if (innerWidth > 800) $sidebar.css({position: `absolute`, top: `calc(${scrollTop}px + var(--nav-height) + var(--padding))`}) })
.on('afterShift', () => $sidebar.css({position: '', top: ''}))
})
.content([
$('section').class('post-info').content([
new $Property('id').name('Post').content(`#${params.id}`),
new $Property('uploader').name('Uploader').content(post.uploader$),
new $Property('approver').name('Approver').content(post.approver$),
new $Property('date').name('Date').content(post.created_date$),
new $Property('size').name('Size').content([post.file_size$, post.dimension$]),
new $Property('file-type').name('File Type').content(post.file_ext$),
$('div').class('inline').content([
new $Property('favorites').name('Favorites').content(post.favcount$),
new $Property('score').name('Score').content(post.score$)
]),
new $Property('file-url').name('File').content([
$('a').href(post.file_url$).content(post.file_url$.convert((value) => value.replace('https://', ''))).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => copyButtonHandler($ion, post.file_url))
]),
new $Property('source-url').name('Source').content([
$('a').href(post.source$).content(post.source$.convert((value) => value.replace('https://', ''))).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => copyButtonHandler($ion, post.source))
]),
new $Property('booru-url').name(Booru.name$).content([
$('a').href(post.booruUrl$).content(post.booruUrl$.convert((value) => value.replace('https://', ''))).target('_blank'),
$('ion-icon').name('clipboard').on('click', (e, $ion) => copyButtonHandler($ion, post.booruUrl))
]),
new $Property('webm-url').name('Webm').hide(true).self(async ($property) => {
await post.ready;
if (post.isUgoria) $property.content($('a').href(post.webm_url$).content(post.webm_url$.convert((value) => value.replace('https://', ''))).target('_blank')).hide(false);
}),
]),
$('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$.convert(numberFormat))
]))
])
] : null
}
})
])
new $DetailPanel().position($route).update(post)
]
})
function copyButtonHandler($ion: $IonIcon, text: string) {
$ion.name('checkmark');
navigator.clipboard.writeText(text);
setTimeout(() => $ion.name('clipboard'), 3000);
}
class $Property extends $Container {
$name = $('span').class('property-name')
$values = $('div').class('property-values')
constructor(id: string) {
super('div');
this.staticClass('property').attribute('property-id', id);
super.content([
this.$name,
this.$values.hide(true)
])
}
name(content: $ContainerContentType) {
this.$name.content(content);
return this;
}
content(content: OrMatrix<$ContainerContentType>) {
this.$values.hide(false);
const list = $.orArrayResolve(content);
this.$values.content(list.map($item => $('span').staticClass('property-value').content($item)));
return this;
}
}
})

View File

@ -1,11 +1,6 @@
#post {
padding: 0;
padding-top: var(--nav-height);
section {
background-color: #2f2f45;
border-radius: var(--border-radius-large);
padding: 20px;
}
div.viewer {
height: calc(100dvh - 2rem - var(--nav-height));
@ -80,13 +75,13 @@
display: flex;
touch-action: none;
align-items: center;
cursor: pointer;
div.progressbar {
height: 0.4rem;
width: 100%;
background-color: var(--secondary-color-1);
flex-shrink: 1;
cursor: pointer;
div.progress {
height: 100%;
@ -156,129 +151,6 @@
}
}
}
div.sidebar {
--padding: 1rem;
position: fixed;
top: calc(var(--nav-height) + var(--padding));
right: var(--padding);
display: flex;
flex-direction: column;
gap: 0.4rem;
width: 300px;
overflow: scroll;
overflow-x: hidden;
height: calc(100dvh - 2rem - var(--nav-height));
border-radius: var(--border-radius-large);
transition: all 0.3s ease;
@media (max-width: 800px) {
position: static;
width: 100%;
overflow: visible;
height: 100%;
padding: 1rem;
box-sizing: border-box;
}
&::-webkit-scrollbar {
background-color: #000000;
width: 0px;
}
&::-webkit-scrollbar-thumb {
background-color: #aeaeec;
}
h3 {
padding-left: 1rem;
margin-block: 0.6rem;
}
.post-info {
background-color: #2f2f45;
border-radius: var(--border-radius-large);
padding: 20px;
display: flex;
flex-direction: column;
gap: 0.4rem;
.buttons {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
margin-top: 1rem;
}
}
div.property {
display: flex;
gap: 0.6rem;
align-items: center;
width: 100%;
span.property-name {
flex-shrink: 0;
}
div.property-values {
display: flex;
gap: 0.4rem;
width: 100%;
overflow: hidden;
span.property-value {
padding: 0.2rem 0.4rem;
background-color: var(--secondary-color-1);
color: var(--primary-color-dark);
border-radius: var(--border-radius-small);
justify-content: space-between;
flex-shrink: 1;
display: flex;
align-items: center;
overflow: hidden;
&:has(ion-icon) {
flex-shrink: 0;
}
* {
display: block;
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
flex-shrink: 1;
}
ion-icon {
font-size: 1rem;
padding: 4px;
box-sizing: border-box;
}
}
}
}
div.inline {
display: flex;
gap: 1rem;
}
div.post-tags {
display: flex;
flex-direction: column;
gap: 0.2rem;
div.tag {
align-items: center;
a.tag-name {
word-break: break-word;
text-decoration: none;
}
span.tag-post-count {
background-color: var(--secondary-color-3);
color: var(--secondary-color-8);
padding: 0px 4px;
border-radius: var(--border-radius-small);
font-size: 12px;
margin-left: 0.4rem;
}
}
}
}
}
// animated resolver

View File

@ -0,0 +1,11 @@
export class LocalSettings {
static get detailPanelEnabled() { return this.localdata?.detailPanelEnabled }
static set detailPanelEnabled(boolean: boolean | undefined) { this.localdata = {...this.localdata, detailPanelEnabled: boolean }}
static get localdata() { const data = localStorage.getItem('local_settings_data'); return data ? JSON.parse(data) as LocalSettingsStoreData : null }
static set localdata(data: LocalSettingsStoreData | null) { localStorage.setItem('local_settings_data', JSON.stringify(data)) }
}
export interface LocalSettingsStoreData {
detailPanelEnabled?: boolean;
}

View File

@ -4,7 +4,6 @@ import { Tag } from "./Tag";
import { User } from "./User";
import { dateFrom, digitalUnit } from "./Util";
import { ClientUser } from "./ClientUser";
import type { FavoriteData } from "./Favorite";
const LOADING_STRING = '...'
@ -132,7 +131,7 @@ export class Post extends $EventManager<{update: []}> {
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 }
get previewURL() { return this.media_asset?.variants?.find(variant => variant.file_ext === 'webp')?.url ?? this.large_file_url }
get booruUrl() { return `${this.booru.origin}/posts/${this.id}` }
get url() { return `https://danbooru.defaultkavy.com/posts/${this.id}` }
get isFileSource() { return this.source.startsWith('file://') }