hippofish/packages/frontend/src/components/MkMediaAudio.vue
ShittyKopper 8a55d8a468 upd: add a download button to videos and audio
this only works for media from the same origin due to annoying browser
restrictions, but then the same applies to every other download button
in misskey (e.g. the one in drive) and there's basically nothing i can
to do solve it.
2024-02-04 12:51:14 +03:00

364 lines
8.3 KiB
Vue

<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
:class="[
$style.audioContainer,
(audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
]"
@contextmenu.stop
>
<button v-if="hide" :class="$style.hidden" @click="hide = false">
<div :class="$style.hiddenTextWrapper">
<b v-if="audio.isSensitive" style="display: block;"><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ph-music-notes ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
</div>
</button>
<div v-else :class="$style.audioControls">
<audio
ref="audioEl"
preload="metadata"
>
<source :src="audio.url">
</audio>
<div :class="[$style.controlsChild, $style.controlsLeft]">
<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
<i v-if="isPlaying" class="ph-pause ph-bold ph-lg"></i>
<i v-else class="ph-play ph-bold ph-lg"></i>
</button>
</div>
<div :class="[$style.controlsChild, $style.controlsRight]">
<a class="_button" :class="$style.controlButton" :href="audio.url" :download="audio.name" target="_blank">
<i class="ph-download ph-bold ph-lg"></i>
</a>
<button class="_button" :class="$style.controlButton" @click="showMenu">
<i class="ph-gear ph-bold ph-lg"></i>
</button>
</div>
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
<div :class="[$style.controlsChild, $style.controlsVolume]">
<button class="_button" :class="$style.controlButton" @click="toggleMute">
<i v-if="volume === 0" class="ph-speaker-x ph-bold ph-lg"></i>
<i v-else class="ph-speaker-high ph-bold ph-lg"></i>
</button>
<MkMediaRange
v-model="volume"
:class="$style.volumeSeekbar"
/>
</div>
<MkMediaRange
v-model="rangePercent"
:class="$style.seekbarRoot"
:buffer="bufferedDataRatio"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import type { MenuItem } from '@/types/menu.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js';
const props = defineProps<{
audio: Misskey.entities.DriveFile;
}>();
const audioEl = shallowRef<HTMLAudioElement>();
// eslint-disable-next-line vue/no-setup-props-destructure
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
// Menu
const menuShowing = ref(false);
function showMenu(ev: MouseEvent) {
let menu: MenuItem[] = [];
menu = [
// TODO: 再生キューに追加
{
text: i18n.ts.hide,
icon: 'ph-eye-closed ph-bold ph-lg',
action: () => {
hide.value = true;
},
},
];
if (iAmModerator) {
menu.push({
type: 'divider',
}, {
text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.audio.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
danger: true,
action: () => toggleSensitive(props.audio),
});
}
menuShowing.value = true;
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right',
onClosing: () => {
menuShowing.value = false;
},
});
}
function toggleSensitive(file: Misskey.entities.DriveFile) {
os.apiWithDialog('drive/files/update', {
fileId: file.id,
isSensitive: !file.isSensitive,
});
}
// 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 (!audioEl.value) return;
audioEl.value.currentTime = to * durationMs.value / 1000;
},
});
const volume = ref(.25);
const bufferedEnd = ref(0);
const bufferedDataRatio = computed(() => {
if (!audioEl.value) return 0;
return bufferedEnd.value / audioEl.value.duration;
});
// MediaControl Events
function togglePlayPause() {
if (!isReady.value || !audioEl.value) return;
if (isPlaying.value) {
audioEl.value.pause();
isPlaying.value = false;
} else {
audioEl.value.play();
isPlaying.value = true;
oncePlayed.value = true;
}
}
function toggleMute() {
if (volume.value === 0) {
volume.value = .25;
} else {
volume.value = 0;
}
}
let onceInit = false;
let stopAudioElWatch: () => void;
function init() {
if (onceInit) return;
onceInit = true;
stopAudioElWatch = watch(audioEl, () => {
if (audioEl.value) {
isReady.value = true;
function updateMediaTick() {
if (audioEl.value) {
try {
bufferedEnd.value = audioEl.value.buffered.end(0);
} catch (err) {
bufferedEnd.value = 0;
}
elapsedTimeMs.value = audioEl.value.currentTime * 1000;
}
window.requestAnimationFrame(updateMediaTick);
}
updateMediaTick();
audioEl.value.addEventListener('play', () => {
isActuallyPlaying.value = true;
});
audioEl.value.addEventListener('pause', () => {
isActuallyPlaying.value = false;
isPlaying.value = false;
});
audioEl.value.addEventListener('ended', () => {
oncePlayed.value = false;
isActuallyPlaying.value = false;
isPlaying.value = false;
});
durationMs.value = audioEl.value.duration * 1000;
audioEl.value.addEventListener('durationchange', () => {
if (audioEl.value) {
durationMs.value = audioEl.value.duration * 1000;
}
});
audioEl.value.volume = volume.value;
}
}, {
immediate: true,
});
}
watch(volume, (to) => {
if (audioEl.value) audioEl.value.volume = to;
});
onMounted(() => {
init();
});
onActivated(() => {
init();
});
onDeactivated(() => {
isReady.value = false;
isPlaying.value = false;
isActuallyPlaying.value = false;
elapsedTimeMs.value = 0;
durationMs.value = 0;
bufferedEnd.value = 0;
hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore');
stopAudioElWatch();
onceInit = false;
});
</script>
<style lang="scss" module>
.audioContainer {
container-type: inline-size;
position: relative;
border: .5px solid var(--divider);
border-radius: var(--radius);
overflow: clip;
}
.sensitive {
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 4px var(--warn);
}
}
.hidden {
width: 100%;
background: #000;
border: none;
outline: none;
font: inherit;
color: inherit;
cursor: pointer;
padding: 12px 0;
display: flex;
align-items: center;
justify-content: center;
}
.hiddenTextWrapper {
text-align: center;
font-size: 0.8em;
color: #fff;
}
.audioControls {
display: grid;
grid-template-areas:
"left time . volume right"
"seekbar seekbar seekbar seekbar seekbar";
grid-template-columns: auto auto 1fr auto auto;
align-items: center;
gap: 4px 8px;
padding: 10px;
}
.controlsChild {
display: flex;
align-items: center;
gap: 4px;
.controlButton {
padding: 6px;
border-radius: calc(var(--radius) / 2);
font-size: 1.05rem;
&:hover {
color: var(--accent);
background-color: var(--accentedBg);
}
}
}
.controlsLeft {
grid-area: left;
}
.controlsRight {
grid-area: right;
}
.controlsTime {
grid-area: time;
font-size: .9rem;
}
.controlsVolume {
grid-area: volume;
.volumeSeekbar {
display: none;
}
}
.seekbarRoot {
grid-area: seekbar;
}
@container (min-width: 500px) {
.audioControls {
grid-template-areas: "left seekbar time volume right";
grid-template-columns: auto 1fr auto auto auto;
}
.controlsVolume {
.volumeSeekbar {
max-width: 90px;
display: block;
flex-grow: 1;
}
}
}
</style>