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');
|
gtag('config', 'G-59HBGP98WR');
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-B5ohILm0.js"></script>
|
<script type="module" crossorigin src="/assets/index-adASBXxZ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-U9yaKy4a.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D689878Y.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "danbooru-viewer",
|
"name": "danbooru-viewer",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.7.0",
|
"version": "0.8.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun x vite",
|
"dev": "bun x vite",
|
||||||
"build": "bun x vite build",
|
"build": "bun x vite build",
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { $Container } from "elexis";
|
import { $Container } from "elexis";
|
||||||
import { Booru } from "../../structure/Booru";
|
import { Booru } from "../../structure/Booru";
|
||||||
import { numberFormat } from "../../modules";
|
import { numberFormat } from "../../structure/Util";
|
||||||
import { danbooru, safebooru } from "../../main";
|
import { danbooru, safebooru } from "../../main";
|
||||||
|
|
||||||
export class $Drawer extends $Container {
|
export class $Drawer extends $Container {
|
||||||
$filter = $('div').class('filter');
|
$filter = $('div').class('filter');
|
||||||
$container = $('div').class('drawer-container')
|
$container = $('div').class('drawer-container')
|
||||||
|
pointers = $.pointers($(document.body));
|
||||||
|
protected opened = false;
|
||||||
constructor() {
|
constructor() {
|
||||||
super('drawer');
|
super('drawer');
|
||||||
this.hide(true);
|
this.hide(true);
|
||||||
@ -52,12 +54,22 @@ export class $Drawer extends $Container {
|
|||||||
]),
|
]),
|
||||||
this.$filter.on('click', () => $.back())
|
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; }
|
open() { if (location.hash !== '#drawer') $.open(location.href + '#drawer'); return this; }
|
||||||
close() { if (location.hash === '#drawer') $.back(); return this; }
|
close() { if (location.hash === '#drawer') $.back(); return this; }
|
||||||
|
|
||||||
private activate() {
|
private activate() {
|
||||||
|
this.opened = true;
|
||||||
this.hide(false);
|
this.hide(false);
|
||||||
this.$container.animate({
|
this.$container.animate({
|
||||||
transform: [`translateX(100%)`, `translateX(0%)`]
|
transform: [`translateX(100%)`, `translateX(0%)`]
|
||||||
@ -76,13 +88,14 @@ export class $Drawer extends $Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private inactivate() {
|
private inactivate() {
|
||||||
|
this.opened = false
|
||||||
this.$container.animate({
|
this.$container.animate({
|
||||||
transform: [`translateX(0%)`, `translateX(100%)`]
|
transform: [`translateX(0%)`, `translateX(100%)`]
|
||||||
}, {
|
}, {
|
||||||
fill: 'both',
|
fill: 'both',
|
||||||
duration: 300,
|
duration: 300,
|
||||||
easing: 'ease'
|
easing: 'ease'
|
||||||
}, () => this.hide(true))
|
}, () => this.hide(!this.opened))
|
||||||
this.$filter.animate({
|
this.$filter.animate({
|
||||||
opacity: [1, 0]
|
opacity: [1, 0]
|
||||||
}, {
|
}, {
|
||||||
|
@ -16,10 +16,8 @@ export class $PostTile extends $Container {
|
|||||||
|
|
||||||
build() {
|
build() {
|
||||||
let timer: Timer
|
let timer: Timer
|
||||||
this.$video?.on('playing', (e, $video) => {
|
this.$video?.on('timeupdate', (e, $video) => {
|
||||||
timer = setInterval(() => {
|
|
||||||
this.durationUpdate();
|
this.durationUpdate();
|
||||||
}, 500)
|
|
||||||
})
|
})
|
||||||
this.$video?.on('pause', () => {
|
this.$video?.on('pause', () => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
|
@ -2,7 +2,7 @@ import { $Container } from "elexis";
|
|||||||
import { Tag, TagCategory } from "../../structure/Tag";
|
import { Tag, TagCategory } from "../../structure/Tag";
|
||||||
import { Booru } from "../../structure/Booru";
|
import { Booru } from "../../structure/Booru";
|
||||||
import { Autocomplete } from "../../structure/Autocomplete";
|
import { Autocomplete } from "../../structure/Autocomplete";
|
||||||
import { numberFormat } from "../../modules";
|
import { numberFormat } from "../../structure/Util";
|
||||||
|
|
||||||
export class $Searchbar extends $Container {
|
export class $Searchbar extends $Container {
|
||||||
$tagInput = new $TagInput(this);
|
$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 { ArtistCommentary } from "../../structure/Commentary";
|
||||||
import { Booru } from "../../structure/Booru";
|
import { Booru } from "../../structure/Booru";
|
||||||
import type { $IonIcon } from "../../component/IonIcon/$IonIcon";
|
import type { $IonIcon } from "../../component/IonIcon/$IonIcon";
|
||||||
import { numberFormat } from "../../modules";
|
import { numberFormat } from "../../structure/Util";
|
||||||
import { ClientUser } from "../../structure/ClientUser";
|
import { ClientUser } from "../../structure/ClientUser";
|
||||||
|
import { $VideoController } from "../../component/VideoController/$VideoController";
|
||||||
|
|
||||||
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 $('h1').content('404: POST NOT FOUND');
|
if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
|
||||||
@ -17,11 +18,14 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
|
|||||||
original_size: []
|
original_size: []
|
||||||
}>();
|
}>();
|
||||||
return [
|
return [
|
||||||
$('div').class('viewer').content(async () => {
|
$('div').class('viewer').content(async ($viewer) => {
|
||||||
|
const $video = $('video');
|
||||||
await post.ready;
|
await post.ready;
|
||||||
return [
|
return [
|
||||||
$('div').class('viewer-panel').hide(true).content([
|
$('div').class('viewer-panel').hide(false).content([
|
||||||
$('div').class('panel').content([
|
$('div').class('panel').content([
|
||||||
|
post.isVideo ? new $VideoController($video, $viewer, post) : null,
|
||||||
|
$('div').class('buttons').content([
|
||||||
$('ion-icon').title('Favorite').name('heart-outline').self($heart => {
|
$('ion-icon').title('Favorite').name('heart-outline').self($heart => {
|
||||||
ClientUser.events.on('favoriteUpdate', (user) => {
|
ClientUser.events.on('favoriteUpdate', (user) => {
|
||||||
if (user.favorites.has(post.id)) $heart.name('heart');
|
if (user.favorites.has(post.id)) $heart.name('heart');
|
||||||
@ -37,6 +41,7 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
|
|||||||
$original.on('click', () => { events.fire('original_size'); $original.disable(true); })
|
$original.on('click', () => { events.fire('original_size'); $original.disable(true); })
|
||||||
if (!post.isLargeFile || post.isVideo) $original.disable(true);
|
if (!post.isLargeFile || post.isVideo) $original.disable(true);
|
||||||
})
|
})
|
||||||
|
])
|
||||||
]),
|
]),
|
||||||
$('div').class('overlay')
|
$('div').class('overlay')
|
||||||
]).self($viewerPanel => {
|
]).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()))
|
.on('viewerPanel_switch', () => $viewerPanel.hide(!$viewerPanel.hide()))
|
||||||
}),
|
}),
|
||||||
post.isVideo
|
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 => {
|
: $('img').src(post.isLargeFile ? post.large_file_url : post.file_url).self($img => {
|
||||||
events.on('original_size', () => $img.src(post.file_url))
|
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');
|
if (e.pointerType === 'mouse' || e.pointerType === 'pen') events.fire('viewerPanel_show');
|
||||||
})
|
})
|
||||||
.on('pointerup', (e) => {
|
.on('pointerup', (e) => {
|
||||||
if (e.pointerType === 'touch') events.fire('viewerPanel_hide');
|
if (e.pointerType === 'touch') events.fire('viewerPanel_switch');
|
||||||
})
|
})
|
||||||
.on('mouseleave', () => {
|
.on('mouseleave', () => {
|
||||||
events.fire('viewerPanel_hide');
|
events.fire('viewerPanel_hide');
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.viewer {
|
div.viewer {
|
||||||
height: calc(100vh - 2rem - var(--nav-height));
|
height: calc(100dvh - 2rem - var(--nav-height));
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -18,10 +18,11 @@
|
|||||||
width: calc(100vw - 300px - 4rem);
|
width: calc(100vw - 300px - 4rem);
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - var(--nav-height));
|
height: calc(100dvh - var(--nav-height));
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin:0;
|
margin:0;
|
||||||
}
|
}
|
||||||
@ -49,9 +50,64 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
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;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div.overlay {
|
div.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -112,8 +168,9 @@
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
height: calc(100vh - 2rem - var(--nav-height));
|
height: calc(100dvh - 2rem - var(--nav-height));
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
position: static;
|
position: static;
|
||||||
|
@ -48,3 +48,8 @@ export function digitalUnit(bytes: number) {
|
|||||||
const eb = bytes / (1000 * 6);
|
const eb = bytes / (1000 * 6);
|
||||||
return `${eb.toFixed(2)}EB`;
|
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',
|
target: 'http://localhost:3030',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
usePolling: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
|
Loading…
Reference in New Issue
Block a user