1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-26 00:55:03 +02:00
Files
misskey/packages/frontend/src/components/MkVideoControl.vue
syuilo 15c187ffa2 enhance(frontend): misskey-native lightbox (#17687)
* wip

* wip

* Update MkImageGallery.item.vue

* wip

* Update MkImageGallery.item.vue

* Update MkImageGallery.item.vue

* wip

* wip

* wip

* wip

* wip

* Update MkImageGallery.vue

* Update MkImageGallery.vue

* wip

* wip

* Update MkMediaList.vue

* Update MkImageGallery.item.vue

* fix

* wip

* Update MkImageGallery.item.vue

* wip

* wip

* Update MkImageGallery.item.vue

* refactor

* Update MkImageGallery.item.vue

* Update MkImageGallery.vue

* Delete animation.ts

* wip

* wip

* Update MkImageGallery.item.vue

* Update MkImageGallery.item.vue

* wip

* wip

* Update MkImageGallery.vue

* Update MkImageGallery.vue

* Update MkImageGallery.item.vue

* fix

* Update MkImageGallery.item.vue

* add note

* lint

* Update MkMediaList.vue

* refactor

* Update MkImageGallery.item.vue

* wip

* Update MkImageGallery.item.vue

* fix

* Update MkImageGallery.item.vue

* wip

* Update MkImageGallery.item.vue

* wip

* wip

* Update MkImageGallery.item.vue

* Update MkImageGallery.item.vue

* Update MkImageGallery.item.vue

* Update MkImageGallery.item.vue

* Update MkVideoContol.vue

* wip

* Update MkMediaVideo.vue

* Update MkMediaVideo.vue

* wip

* Update MkImageGallery.vue

* Update MkVideoContol.vue

* Update MkVideoContol.vue

* Improve image viewer (#17696)

* wip

* fix

* fix

* 🎨

* fix

* fix: Video要素をpropsで渡すのをやめる

* 🎨

* fix

* wip

* fix

* fix: 画面外にドラッグできるのを修正

* fix lint

* fix

* fix

* fix

* refactor

* add comment

* fix: 画像を新しいタブで開くが機能しなくなっていた問題を修正

* fix

* clean up

* attempt to fix

* fix

* enhance: ブラウザの戻るボタンで閉じる

* Revert "fix: 画面外にドラッグできるのを修正"

This reverts commit ea2ddc2620.

* Reapply "fix: 画面外にドラッグできるのを修正"

This reverts commit 87171980a7.

* fix

* fix: 存在しない要素がsourceElementのときに閉じると画像が飛んでいく問題を修正

* fix: clampに余白を追加

* fix duplicated condition

* fix

* fix typo

* fix(frontend/imagegallery): ボタンの当たり判定がまだ小さいのを修正 (#17697)

* wip

* fix

* fix

* 🎨

* fix

* fix: Video要素をpropsで渡すのをやめる

* 🎨

* fix

* wip

* fix

* fix: 画面外にドラッグできるのを修正

* fix lint

* fix

* fix

* fix

* refactor

* add comment

* fix: 画像を新しいタブで開くが機能しなくなっていた問題を修正

* fix

* clean up

* attempt to fix

* fix

* enhance: ブラウザの戻るボタンで閉じる

* Revert "fix: 画面外にドラッグできるのを修正"

This reverts commit ea2ddc2620.

* Reapply "fix: 画面外にドラッグできるのを修正"

This reverts commit 87171980a7.

* fix

* fix: 存在しない要素がsourceElementのときに閉じると画像が飛んでいく問題を修正

* fix: clampに余白を追加

* fix duplicated condition

* fix

* fix typo

* fix

* Update Changelog

* indent

* fix

* fix

* refactor: rename componet

* fix

* fix

* fix

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2026-07-12 11:58:44 +09:00

319 lines
6.9 KiB
Vue

<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div :class="[$style.seekbar]">
<MkMediaRange
v-model="rangePercent"
:buffer="bufferedDataRatio"
/>
</div>
<div :class="[$style.controlsChild, $style.controlsLeft]">
<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
<i v-if="isPlaying" class="ti ti-player-pause"></i>
<i v-else class="ti ti-player-play"></i>
</button>
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }} / {{ hms(durationMs) }}</div>
</div>
<div :class="[$style.controlsChild, $style.controlsCenter]">
</div>
<div :class="[$style.controlsChild, $style.controlsRight]">
<button class="_button" :class="$style.controlButton" @click="toggleMute">
<i v-if="volume === 0" class="ti ti-volume-3"></i>
<i v-else class="ti ti-volume"></i>
</button>
<MkMediaRange
v-model="volume"
:class="$style.volumeSeekbar"
/>
<button class="_button" :class="$style.controlButton" @click="showMenu">
<i class="ti ti-settings"></i>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, inject, computed, watch, onBeforeUnmount } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import { DI } from '@/di.js';
import { hms } from '@/filters/hms.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import hasAudio from '@/utility/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
const videoEl = inject(DI.mkLightboxItemVideoEl, shallowRef<HTMLVideoElement | null>(null));
// Menu
const menuShowing = ref(false);
function showMenu(ev: PointerEvent) {
const menu: MenuItem[] = [
// TODO: 再生キューに追加
{
type: 'switch',
text: i18n.ts._mediaControls.loop,
icon: 'ti ti-repeat',
ref: loop,
},
{
type: 'radio',
text: i18n.ts._mediaControls.playbackRate,
icon: 'ti ti-clock-play',
ref: speed,
options: [{
label: '0.25x',
value: 0.25,
}, {
label: '0.5x',
value: 0.5,
}, {
label: '0.75x',
value: 0.75,
}, {
label: '1.0x',
value: 1,
}, {
label: '1.25x',
value: 1.25,
}, {
label: '1.5x',
value: 1.5,
}, {
label: '2.0x',
value: 2,
}],
},
...(window.document.pictureInPictureEnabled ? [{
text: i18n.ts._mediaControls.pip,
icon: 'ti ti-picture-in-picture',
action: togglePictureInPicture,
}] : []),
];
menuShowing.value = true;
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right',
onClosing: () => {
menuShowing.value = false;
},
});
}
// MediaControl: Common State
const oncePlayed = ref(false);
const isReady = ref(false);
const isPlaying = ref(false);
const isActuallyPlaying = ref(false);
const elapsedTimeMs = ref(0);
const durationMs = ref(0);
const rangePercent = computed({
get: () => {
return (elapsedTimeMs.value / durationMs.value) || 0;
},
set: (to) => {
if (videoEl.value == null) return;
videoEl.value.currentTime = to * durationMs.value / 1000;
},
});
const volume = ref(.25);
const speed = ref(1);
const loop = ref(false); // TODO: ドライブファイルのフラグに置き換える
const bufferedEnd = ref(0);
const bufferedDataRatio = computed(() => {
if (videoEl.value == null || videoEl.value.duration === 0) return 0;
return bufferedEnd.value / videoEl.value.duration;
});
function togglePlayPause() {
if (!isReady.value) return;
if (isPlaying.value) {
videoEl.value?.pause();
isPlaying.value = false;
} else {
videoEl.value?.play();
isPlaying.value = true;
oncePlayed.value = true;
}
}
function togglePictureInPicture() {
if (window.document.pictureInPictureElement) {
window.document.exitPictureInPicture();
} else {
videoEl.value?.requestPictureInPicture();
}
}
function toggleMute() {
if (volume.value === 0) {
volume.value = .25;
} else {
volume.value = 0;
}
}
let abortController: AbortController | null = null;
let mediaTickFrameId: number | null = null;
function init() {
if (videoEl.value == null) return;
isReady.value = true;
abortController = new AbortController();
function updateMediaTick() {
if (videoEl.value == null) return;
try {
bufferedEnd.value = videoEl.value.buffered.end(0);
} catch (err) {
bufferedEnd.value = 0;
}
elapsedTimeMs.value = videoEl.value.currentTime * 1000;
if (videoEl.value.loop !== loop.value) {
loop.value = videoEl.value.loop;
}
if (videoEl.value.paused !== !isPlaying.value) {
isPlaying.value = !videoEl.value.paused;
}
mediaTickFrameId = window.requestAnimationFrame(updateMediaTick);
}
updateMediaTick();
videoEl.value.addEventListener('waiting', () => {
isActuallyPlaying.value = false;
}, { signal: abortController.signal });
videoEl.value.addEventListener('playing', () => {
isActuallyPlaying.value = true;
}, { signal: abortController.signal });
videoEl.value.addEventListener('pause', () => {
isActuallyPlaying.value = false;
isPlaying.value = false;
}, { signal: abortController.signal });
videoEl.value.addEventListener('ended', () => {
oncePlayed.value = false;
isActuallyPlaying.value = false;
isPlaying.value = false;
}, { signal: abortController.signal });
durationMs.value = videoEl.value.duration * 1000;
videoEl.value.addEventListener('durationchange', () => {
durationMs.value = videoEl.value!.duration * 1000;
}, { signal: abortController.signal });
videoEl.value.volume = volume.value;
hasAudio(videoEl.value).then(had => {
if (!had) {
videoEl.value!.loop = videoEl.value!.muted = true;
videoEl.value!.play();
}
});
}
watch(volume, (to) => {
if (videoEl.value == null) return;
videoEl.value.volume = to;
});
watch(speed, (to) => {
if (videoEl.value == null) return;
videoEl.value.playbackRate = to;
});
watch(loop, (to) => {
if (videoEl.value == null) return;
videoEl.value.loop = to;
});
watch(videoEl, () => {
if (abortController != null) {
abortController.abort();
}
if (mediaTickFrameId != null) {
window.cancelAnimationFrame(mediaTickFrameId);
}
init();
}, { immediate: true });
onBeforeUnmount(() => {
if (mediaTickFrameId != null) {
window.cancelAnimationFrame(mediaTickFrameId);
}
});
defineExpose({
isPlaying,
isActuallyPlaying,
});
</script>
<style lang="scss" module>
.root {
display: grid;
grid-template-areas:
"seekbar seekbar seekbar"
"left center right";
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 4px 8px;
width: 100%;
}
.controlsChild {
display: flex;
align-items: center;
gap: 4px;
}
.controlsLeft {
grid-area: left;
}
.controlsRight {
grid-area: right;
}
.controlsCenter {
grid-area: center;
justify-content: center;
}
.controlButton {
padding: 6px;
border-radius: 4px;
&:hover {
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
}
&:focus-visible {
outline: none;
}
}
.controlsTime {
font-size: 85%;
}
.seekbar {
grid-area: seekbar;
}
</style>