v0.8.0
new: touch or pen gesture open/close $Drawer. optimize: move numberFormat function to Util. new: video controller. optimize: $PostTile video duration update by timeupdate event. change: post viewer height depend on dynamic viewport height.
This commit is contained in:
parent
580ac885de
commit
93b06d7c80
1
dist/assets/index-B5ohILm0.js
vendored
1
dist/assets/index-B5ohILm0.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-D689878Y.css
vendored
Normal file
1
dist/assets/index-D689878Y.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-U9yaKy4a.css
vendored
1
dist/assets/index-U9yaKy4a.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-adASBXxZ.js
vendored
Normal file
1
dist/assets/index-adASBXxZ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@ -16,8 +16,8 @@
|
||||
|
||||
gtag('config', 'G-59HBGP98WR');
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-B5ohILm0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-U9yaKy4a.css">
|
||||
<script type="module" crossorigin src="/assets/index-adASBXxZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D689878Y.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "danbooru-viewer",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"scripts": {
|
||||
"dev": "bun x vite",
|
||||
"build": "bun x vite build",
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { $Container } from "elexis";
|
||||
import { Booru } from "../../structure/Booru";
|
||||
import { numberFormat } from "../../modules";
|
||||
import { numberFormat } from "../../structure/Util";
|
||||
import { danbooru, safebooru } from "../../main";
|
||||
|
||||
export class $Drawer extends $Container {
|
||||
$filter = $('div').class('filter');
|
||||
$container = $('div').class('drawer-container')
|
||||
pointers = $.pointers($(document.body));
|
||||
protected opened = false;
|
||||
constructor() {
|
||||
super('drawer');
|
||||
this.hide(true);
|
||||
@ -52,12 +54,22 @@ export class $Drawer extends $Container {
|
||||
]),
|
||||
this.$filter.on('click', () => $.back())
|
||||
])
|
||||
|
||||
this.pointers.on('move', pointer => {
|
||||
if ($(':.viewer')?.contains(pointer.$target)) return;
|
||||
pointer.$target.parent
|
||||
if (pointer.type !== 'pen' && pointer.type !== 'touch') return;
|
||||
if (pointer.move_y > 4 || pointer.move_y < -4) return;
|
||||
if (pointer.move_x <= -7) { pointer.delete(); this.open(); }
|
||||
if (pointer.move_x >= 7) { pointer.delete(); this.close(); }
|
||||
})
|
||||
}
|
||||
|
||||
open() { if (location.hash !== '#drawer') $.open(location.href + '#drawer'); return this; }
|
||||
close() { if (location.hash === '#drawer') $.back(); return this; }
|
||||
|
||||
private activate() {
|
||||
this.opened = true;
|
||||
this.hide(false);
|
||||
this.$container.animate({
|
||||
transform: [`translateX(100%)`, `translateX(0%)`]
|
||||
@ -76,13 +88,14 @@ export class $Drawer extends $Container {
|
||||
}
|
||||
|
||||
private inactivate() {
|
||||
this.opened = false
|
||||
this.$container.animate({
|
||||
transform: [`translateX(0%)`, `translateX(100%)`]
|
||||
}, {
|
||||
fill: 'both',
|
||||
duration: 300,
|
||||
easing: 'ease'
|
||||
}, () => this.hide(true))
|
||||
}, () => this.hide(!this.opened))
|
||||
this.$filter.animate({
|
||||
opacity: [1, 0]
|
||||
}, {
|
||||
|
@ -16,10 +16,8 @@ export class $PostTile extends $Container {
|
||||
|
||||
build() {
|
||||
let timer: Timer
|
||||
this.$video?.on('playing', (e, $video) => {
|
||||
timer = setInterval(() => {
|
||||
this.durationUpdate();
|
||||
}, 500)
|
||||
this.$video?.on('timeupdate', (e, $video) => {
|
||||
this.durationUpdate();
|
||||
})
|
||||
this.$video?.on('pause', () => {
|
||||
clearInterval(timer);
|
||||
|
@ -2,7 +2,7 @@ import { $Container } from "elexis";
|
||||
import { Tag, TagCategory } from "../../structure/Tag";
|
||||
import { Booru } from "../../structure/Booru";
|
||||
import { Autocomplete } from "../../structure/Autocomplete";
|
||||
import { numberFormat } from "../../modules";
|
||||
import { numberFormat } from "../../structure/Util";
|
||||
|
||||
export class $Searchbar extends $Container {
|
||||
$tagInput = new $TagInput(this);
|
||||
|
103
src/component/VideoController/$VideoController.ts
Normal file
103
src/component/VideoController/$VideoController.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { $Container, $Node, type $Video } from "elexis";
|
||||
import { time } from "../../structure/Util";
|
||||
import type { Post } from "../../structure/Post";
|
||||
|
||||
export class $VideoController extends $Container {
|
||||
$video: $Video;
|
||||
$viewer: $Container;
|
||||
duration$ = $.state('00:00');
|
||||
post: Post;
|
||||
constructor($video: $Video, $viewer: $Container, post: Post) {
|
||||
super('video-controller')
|
||||
this.$video = $video
|
||||
this.$viewer = $viewer;
|
||||
this.post = post;
|
||||
this.build();
|
||||
}
|
||||
|
||||
protected build() {
|
||||
const events = $.events<{
|
||||
progressChange: [number]
|
||||
}>();
|
||||
this.$video.on('timeupdate', () => this.durationUpdate())
|
||||
this.content([
|
||||
$('div').class('video-details').content([
|
||||
$('div').class('left').content([
|
||||
$('ion-icon').class('play').title('Play').name('play').self($play => {
|
||||
this.$video.on('play', () => $play.name('pause'))
|
||||
.on('pause', () => $play.name('play'))
|
||||
$play.on('click', () => this.$video.isPlaying ? this.$video.pause() : this.$video.play())
|
||||
}),
|
||||
$('div').class('duration').content([
|
||||
$('span').class('current-time').content(this.duration$),
|
||||
$('span').content('/'),
|
||||
$('span').class('total-time').content('00:00').self($time => {
|
||||
this.$video.on('loadeddata', () => {
|
||||
const t = time(this.$video.duration * 1000);
|
||||
$time.content(Number(t.hh) > 0 ? `${t.hh}:${t.mm}:${t.ss}` : `${t.mm}:${t.ss}`);
|
||||
})
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
$('div').class('right').content([
|
||||
$('ion-icon').class('volume').title('Volume').name('volume-high').disable(!this.post.hasSound).self($volume => {
|
||||
const check = () => {
|
||||
if (this.$video.muted()) $volume.name('volume-mute');
|
||||
else $volume.name('volume-high');
|
||||
}
|
||||
$volume.on('click', () => {
|
||||
this.$video.muted(!this.$video.muted())
|
||||
check();
|
||||
})
|
||||
}),
|
||||
$('ion-icon').class('full-screen').title('Full-Screen').name('scan').self($fullscreen => {
|
||||
$fullscreen.on('click', () => {
|
||||
if (document.fullscreenElement) document.exitFullscreen()
|
||||
else this.$viewer.dom.requestFullscreen()
|
||||
})
|
||||
})
|
||||
])
|
||||
]),
|
||||
$('div').class('progressbar-container').content([
|
||||
$('div').class('progressbar').content([
|
||||
$('div').class('progress').self($progress => {
|
||||
this.$video.on('timeupdate', e => {
|
||||
$progress.css({width: `${(this.$video.currentTime() / this.$video.duration) * 100}%`})
|
||||
})
|
||||
events.on('progressChange', percentage => {
|
||||
$progress.css({width: `${percentage * 100}%`})
|
||||
})
|
||||
})
|
||||
])
|
||||
]).self($bar => {
|
||||
const pointers = $.pointers($(document.body));
|
||||
let isPlaying = false;
|
||||
pointers.on('down', (pointer, e) => {
|
||||
if (!$bar.contains(pointer.$target)) return pointer.delete();
|
||||
e.preventDefault()
|
||||
if (this.$video.isPlaying) {
|
||||
isPlaying = true;
|
||||
this.$video.pause();
|
||||
}
|
||||
const percentage = (pointer.x - $bar.domRect().x) / $bar.offsetWidth;
|
||||
this.$video.currentTime(percentage * this.$video.duration);
|
||||
})
|
||||
pointers.on('move', (pointer, e) => {
|
||||
e.preventDefault()
|
||||
const percentage = (pointer.x - $bar.domRect().x) / $bar.offsetWidth;
|
||||
this.$video.currentTime(percentage * this.$video.duration);
|
||||
events.fire('progressChange', percentage)
|
||||
})
|
||||
pointers.on('up', (pointer, e) => {
|
||||
if (isPlaying) this.$video.play();
|
||||
isPlaying = false;
|
||||
})
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
durationUpdate() {
|
||||
const t = time(this.$video.currentTime() * 1000)
|
||||
this.duration$.set(Number(t.hh) > 0 ? `${t.hh}:${t.mm}:${t.ss}` : `${t.mm}:${t.ss}`)
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
const NUMBER_FORMAT = new Intl.NumberFormat('en', {notation: 'compact'})
|
||||
export function numberFormat(number: number) {
|
||||
return NUMBER_FORMAT.format(number)
|
||||
}
|
@ -4,8 +4,9 @@ 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 "../../modules";
|
||||
import { numberFormat } from "../../structure/Util";
|
||||
import { ClientUser } from "../../structure/ClientUser";
|
||||
import { $VideoController } from "../../component/VideoController/$VideoController";
|
||||
|
||||
export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => {
|
||||
if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
|
||||
@ -17,26 +18,30 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
|
||||
original_size: []
|
||||
}>();
|
||||
return [
|
||||
$('div').class('viewer').content(async () => {
|
||||
$('div').class('viewer').content(async ($viewer) => {
|
||||
const $video = $('video');
|
||||
await post.ready;
|
||||
return [
|
||||
$('div').class('viewer-panel').hide(true).content([
|
||||
$('div').class('viewer-panel').hide(false).content([
|
||||
$('div').class('panel').content([
|
||||
$('ion-icon').title('Favorite').name('heart-outline').self($heart => {
|
||||
ClientUser.events.on('favoriteUpdate', (user) => {
|
||||
if (user.favorites.has(post.id)) $heart.name('heart');
|
||||
else $heart.name('heart-outline');
|
||||
post.isVideo ? new $VideoController($video, $viewer, post) : null,
|
||||
$('div').class('buttons').content([
|
||||
$('ion-icon').title('Favorite').name('heart-outline').self($heart => {
|
||||
ClientUser.events.on('favoriteUpdate', (user) => {
|
||||
if (user.favorites.has(post.id)) $heart.name('heart');
|
||||
else $heart.name('heart-outline');
|
||||
})
|
||||
if (Booru.used.user?.favorites.has(post.id)) $heart.name('heart');
|
||||
$heart.on('click', () => {
|
||||
if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite();
|
||||
else post.createFavorite();
|
||||
})
|
||||
}),
|
||||
$('ion-icon').title('Original Size').name('resize-outline').self($original => {
|
||||
$original.on('click', () => { events.fire('original_size'); $original.disable(true); })
|
||||
if (!post.isLargeFile || post.isVideo) $original.disable(true);
|
||||
})
|
||||
if (Booru.used.user?.favorites.has(post.id)) $heart.name('heart');
|
||||
$heart.on('click', () => {
|
||||
if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite();
|
||||
else post.createFavorite();
|
||||
})
|
||||
}),
|
||||
$('ion-icon').title('Original Size').name('resize-outline').self($original => {
|
||||
$original.on('click', () => { events.fire('original_size'); $original.disable(true); })
|
||||
if (!post.isLargeFile || post.isVideo) $original.disable(true);
|
||||
})
|
||||
])
|
||||
]),
|
||||
$('div').class('overlay')
|
||||
]).self($viewerPanel => {
|
||||
@ -45,7 +50,7 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
|
||||
.on('viewerPanel_switch', () => $viewerPanel.hide(!$viewerPanel.hide()))
|
||||
}),
|
||||
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).disablePictureInPicture(true)
|
||||
? $video.height(post.image_height).width(post.image_width).src(post.file_ext === 'zip' ? post.large_file_url : post.file_url).controls(false).autoplay(true).loop(true).disablePictureInPicture(true)
|
||||
: $('img').src(post.isLargeFile ? post.large_file_url : post.file_url).self($img => {
|
||||
events.on('original_size', () => $img.src(post.file_url))
|
||||
})
|
||||
@ -56,7 +61,7 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
|
||||
if (e.pointerType === 'mouse' || e.pointerType === 'pen') events.fire('viewerPanel_show');
|
||||
})
|
||||
.on('pointerup', (e) => {
|
||||
if (e.pointerType === 'touch') events.fire('viewerPanel_hide');
|
||||
if (e.pointerType === 'touch') events.fire('viewerPanel_switch');
|
||||
})
|
||||
.on('mouseleave', () => {
|
||||
events.fire('viewerPanel_hide');
|
||||
|
@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
div.viewer {
|
||||
height: calc(100vh - 2rem - var(--nav-height));
|
||||
height: calc(100dvh - 2rem - var(--nav-height));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -18,10 +18,11 @@
|
||||
width: calc(100vw - 300px - 4rem);
|
||||
margin: 1rem;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--nav-height));
|
||||
height: calc(100dvh - var(--nav-height));
|
||||
border-radius: 0;
|
||||
margin:0;
|
||||
}
|
||||
@ -49,8 +50,63 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
gap: 2rem;
|
||||
gap: 1rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
video-controller {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
div.video-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
div.progressbar-container {
|
||||
height: 2rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
touch-action: none;
|
||||
align-items: center;
|
||||
|
||||
div.progressbar {
|
||||
height: 0.4rem;
|
||||
width: 100%;
|
||||
background-color: var(--secondary-color-1);
|
||||
flex-shrink: 1;
|
||||
cursor: pointer;
|
||||
|
||||
div.progress {
|
||||
height: 100%;
|
||||
background-color: var(--secondary-color-3);
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
div.buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
div.overlay {
|
||||
@ -112,8 +168,9 @@
|
||||
width: 300px;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - 2rem - var(--nav-height));
|
||||
height: calc(100dvh - 2rem - var(--nav-height));
|
||||
border-radius: var(--border-radius-large);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
position: static;
|
||||
|
@ -47,4 +47,9 @@ export function digitalUnit(bytes: number) {
|
||||
if (pb < 1000) return `${pb.toFixed(2)}PB`;
|
||||
const eb = bytes / (1000 * 6);
|
||||
return `${eb.toFixed(2)}EB`;
|
||||
}
|
||||
|
||||
const NUMBER_FORMAT = new Intl.NumberFormat('en', {notation: 'compact'})
|
||||
export function numberFormat(number: number) {
|
||||
return NUMBER_FORMAT.format(number)
|
||||
}
|
@ -6,6 +6,9 @@ export default defineConfig({
|
||||
target: 'http://localhost:3030',
|
||||
changeOrigin: true
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
usePolling: true
|
||||
}
|
||||
},
|
||||
define: {
|
||||
|
Loading…
Reference in New Issue
Block a user