feat(frontend): Audio player で波形を表示するように (MisskeyIO#827)
Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com> Co-authored-by: tar_bin <tar.bin.master@gmail.com>
This commit is contained in:
parent
b5df2c0356
commit
714aad6312
11 changed files with 349 additions and 44 deletions
|
@ -76,7 +76,8 @@
|
||||||
"vite": "5.4.11",
|
"vite": "5.4.11",
|
||||||
"vue": "3.5.13",
|
"vue": "3.5.13",
|
||||||
"vue-gtag": "2.0.1",
|
"vue-gtag": "2.0.1",
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next",
|
||||||
|
"webgl-audiovisualizer": "github:tar-bin/webgl-audiovisualizer"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||||
|
@ -107,6 +108,7 @@
|
||||||
"@types/node": "22.10.0",
|
"@types/node": "22.10.0",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
"@types/sanitize-html": "2.13.0",
|
"@types/sanitize-html": "2.13.0",
|
||||||
|
"@types/three": "0.170.0",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/ws": "8.5.13",
|
"@types/ws": "8.5.13",
|
||||||
|
|
219
packages/frontend/src/components/MkAudioVisualizer.vue
Normal file
219
packages/frontend/src/components/MkAudioVisualizer.vue
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
<template>
|
||||||
|
<div ref="container" :class="$style.root">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, onUnmounted, shallowRef, nextTick, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import vertexShader from '../../node_modules/webgl-audiovisualizer/audiovisial-vertex.shader?raw';
|
||||||
|
import fragmentShader from '../../node_modules/webgl-audiovisualizer/audiovisial-fragment.shader?raw';
|
||||||
|
import circleMask from '../../node_modules/webgl-audiovisualizer/circlemask.png';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: Misskey.entities.UserLite;
|
||||||
|
audioEl: HTMLAudioElement | undefined;
|
||||||
|
analyser: AnalyserNode | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const container = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
const isPlaying = ref(false);
|
||||||
|
const fftSize = 2048;
|
||||||
|
|
||||||
|
let prevTime = 0;
|
||||||
|
let angle1 = 0;
|
||||||
|
let angle2 = 0;
|
||||||
|
|
||||||
|
let scene, camera, renderer, width, height, uniforms, texture, maskTexture, dataArray1, dataArray2, dataArrayOrigin, bufferLength: number;
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
const parent = container.value ?? { offsetWidth: 0 };
|
||||||
|
width = parent.offsetWidth;
|
||||||
|
height = Math.floor(width * 9 / 16);
|
||||||
|
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
camera = new THREE.OrthographicCamera();
|
||||||
|
camera.left = width / -2;
|
||||||
|
camera.right = width / 2;
|
||||||
|
camera.top = height / 2;
|
||||||
|
camera.bottom = height / -2;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
|
||||||
|
if (container.value) {
|
||||||
|
container.value.appendChild(renderer.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = new THREE.TextureLoader();
|
||||||
|
texture = loader.load(props.user.avatarUrl ?? '');
|
||||||
|
maskTexture = loader.load(circleMask);
|
||||||
|
uniforms = {
|
||||||
|
enableAudio: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
uTex: { value: texture },
|
||||||
|
uMask: { value: maskTexture },
|
||||||
|
time: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
resolution: {
|
||||||
|
value: new THREE.Vector2(width, height),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const material = new THREE.ShaderMaterial({
|
||||||
|
uniforms: uniforms,
|
||||||
|
vertexShader: vertexShader,
|
||||||
|
fragmentShader: fragmentShader,
|
||||||
|
});
|
||||||
|
|
||||||
|
const geometry = new THREE.PlaneGeometry(2, 2);
|
||||||
|
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
renderer.setAnimationLoop(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const play = () => {
|
||||||
|
if (props.analyser) {
|
||||||
|
bufferLength = props.analyser.frequencyBinCount;
|
||||||
|
dataArrayOrigin = new Uint8Array(bufferLength);
|
||||||
|
dataArray1 = new Uint8Array(bufferLength);
|
||||||
|
dataArray2 = new Uint8Array(bufferLength);
|
||||||
|
uniforms = {
|
||||||
|
enableAudio: {
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
tAudioData1: { value: new THREE.DataTexture(dataArray1, fftSize / 2, 1, THREE.RedFormat) },
|
||||||
|
tAudioData2: { value: new THREE.DataTexture(dataArray2, fftSize / 2, 1, THREE.RedFormat) },
|
||||||
|
uTex: { value: texture },
|
||||||
|
uMask: { value: maskTexture },
|
||||||
|
time: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
resolution: {
|
||||||
|
value: new THREE.Vector2(width, height),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const material = new THREE.ShaderMaterial({
|
||||||
|
uniforms: uniforms,
|
||||||
|
vertexShader: vertexShader,
|
||||||
|
fragmentShader: fragmentShader,
|
||||||
|
});
|
||||||
|
|
||||||
|
const geometry = new THREE.PlaneGeometry(2, 2);
|
||||||
|
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
renderer.setAnimationLoop(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pauseAnimation = () => {
|
||||||
|
isPlaying.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resumeAnimation = () => {
|
||||||
|
isPlaying.value = true;
|
||||||
|
renderer.setAnimationLoop(play);
|
||||||
|
};
|
||||||
|
|
||||||
|
const animate = (time) => {
|
||||||
|
if (angle1 >= bufferLength) {
|
||||||
|
angle1 = 0;
|
||||||
|
}
|
||||||
|
if (angle2 >= bufferLength) {
|
||||||
|
angle2 = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.analyser) {
|
||||||
|
if ((time - prevTime) > 10) {
|
||||||
|
for (let i = 0; i < bufferLength; i++) {
|
||||||
|
let n1 = (i + angle1) % bufferLength;
|
||||||
|
let n2 = (i + angle2) % bufferLength;
|
||||||
|
if (dataArrayOrigin[n1] > dataArray1[i]) {
|
||||||
|
dataArray1[i] = dataArray1[i] < 255 ? (dataArrayOrigin[i] + dataArray1[i]) / 2 : 255;
|
||||||
|
}
|
||||||
|
if (dataArrayOrigin[n2] > dataArray2[i]) {
|
||||||
|
dataArray2[i] = dataArray2[i] < 255 ? (dataArrayOrigin[i] + dataArray2[i]) / 2 : 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((time - prevTime) > 20) {
|
||||||
|
for (let i = 0; i < bufferLength; i++) {
|
||||||
|
let n1 = (i + angle1) % bufferLength;
|
||||||
|
let n2 = (i + angle2) % bufferLength;
|
||||||
|
if (dataArrayOrigin[n1] < dataArray1[i]) {
|
||||||
|
dataArray1[i] = dataArray1[i] > 0 ? dataArray1[i] - 1 : 0;
|
||||||
|
}
|
||||||
|
if (dataArrayOrigin[n2] < dataArray2[i]) {
|
||||||
|
dataArray2[i] = dataArray2[i] > 0 ? dataArray2[i] - 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uniforms.tAudioData1.value.needsUpdate = true;
|
||||||
|
uniforms.tAudioData2.value.needsUpdate = true;
|
||||||
|
prevTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlaying.value) {
|
||||||
|
props.analyser.getByteTimeDomainData(dataArrayOrigin);
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < bufferLength; i++) {
|
||||||
|
dataArrayOrigin[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angle1 += parseInt(String(bufferLength / 360 * 2)); //こうしないと型エラー出てうざい
|
||||||
|
angle2 += parseInt(String(bufferLength / 360 * 3));
|
||||||
|
}
|
||||||
|
uniforms.time.value = time;
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
const parent = container.value ?? { offsetWidth: 0 };
|
||||||
|
width = parent.offsetWidth;
|
||||||
|
height = Math.floor(width * 9 / 16);
|
||||||
|
camera.left = width / -2;
|
||||||
|
camera.right = width / 2;
|
||||||
|
camera.top = height / 2;
|
||||||
|
camera.bottom = height / -2;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
uniforms.resolution.value.set(width, height);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
nextTick().then(() => {
|
||||||
|
init();
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (renderer) {
|
||||||
|
renderer.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
pauseAnimation,
|
||||||
|
resumeAnimation,
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -35,40 +35,43 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else :class="$style.audioControls">
|
<div v-else>
|
||||||
<audio
|
<MkAudioVisualizer v-if="user" ref="audioVisualizer" :audioEl="audioEl" :analyser="analyserNode" :user="user" :profileImage="user.avatarUrl"/>
|
||||||
ref="audioEl"
|
<div :class="$style.audioControls">
|
||||||
preload="metadata"
|
<audio
|
||||||
>
|
ref="audioEl"
|
||||||
<source :src="audio.url">
|
preload="metadata"
|
||||||
</audio>
|
>
|
||||||
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
<source :src="audio.url">
|
||||||
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
|
</audio>
|
||||||
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
||||||
<i v-else class="ti ti-player-play-filled"></i>
|
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
|
||||||
</button>
|
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
||||||
</div>
|
<i v-else class="ti ti-player-play-filled"></i>
|
||||||
<div :class="[$style.controlsChild, $style.controlsRight]">
|
</button>
|
||||||
<button class="_button" :class="$style.controlButton" @click.prevent.stop="showMenu">
|
</div>
|
||||||
<i class="ti ti-settings"></i>
|
<div :class="[$style.controlsChild, $style.controlsRight]">
|
||||||
</button>
|
<button class="_button" :class="$style.controlButton" @click.prevent.stop="showMenu">
|
||||||
</div>
|
<i class="ti ti-settings"></i>
|
||||||
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
|
</button>
|
||||||
<div :class="[$style.controlsChild, $style.controlsVolume]">
|
</div>
|
||||||
<button class="_button" :class="$style.controlButton" @click.prevent.stop="toggleMute">
|
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
|
||||||
<i v-if="volume === 0" class="ti ti-volume-3"></i>
|
<div :class="[$style.controlsChild, $style.controlsVolume]">
|
||||||
<i v-else class="ti ti-volume"></i>
|
<button class="_button" :class="$style.controlButton" @click.prevent.stop="toggleMute">
|
||||||
</button>
|
<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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<MkMediaRange
|
<MkMediaRange
|
||||||
v-model="volume"
|
v-model="rangePercent"
|
||||||
:class="$style.volumeSeekbar"
|
:class="$style.seekbarRoot"
|
||||||
|
:buffer="bufferedDataRatio"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MkMediaRange
|
|
||||||
v-model="rangePercent"
|
|
||||||
:class="$style.seekbarRoot"
|
|
||||||
:buffer="bufferedDataRatio"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -82,12 +85,14 @@ import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import { hms } from '@/filters/hms.js';
|
import { hms } from '@/filters/hms.js';
|
||||||
|
import MkAudioVisualizer from '@/components/MkAudioVisualizer.vue';
|
||||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import { $i, iAmModerator } from '@/account.js';
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
audio: Misskey.entities.DriveFile;
|
audio: Misskey.entities.DriveFile;
|
||||||
|
user?: Misskey.entities.UserLite;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
|
@ -126,6 +131,7 @@ function hasFocus() {
|
||||||
|
|
||||||
const playerEl = shallowRef<HTMLDivElement>();
|
const playerEl = shallowRef<HTMLDivElement>();
|
||||||
const audioEl = shallowRef<HTMLAudioElement>();
|
const audioEl = shallowRef<HTMLAudioElement>();
|
||||||
|
const audioVisualizer = ref<InstanceType<typeof MkAudioVisualizer>>();
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
// 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'));
|
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
|
||||||
|
@ -240,6 +246,11 @@ const isPlaying = ref(false);
|
||||||
const isActuallyPlaying = ref(false);
|
const isActuallyPlaying = ref(false);
|
||||||
const elapsedTimeMs = ref(0);
|
const elapsedTimeMs = ref(0);
|
||||||
const durationMs = ref(0);
|
const durationMs = ref(0);
|
||||||
|
const audioContext = ref<AudioContext | null>(null);
|
||||||
|
const sourceNode = ref<MediaElementAudioSourceNode | null>(null);
|
||||||
|
const gainNode = ref<GainNode | null>(null);
|
||||||
|
const analyserGainNode = ref<GainNode | null>(null);
|
||||||
|
const analyserNode = ref<AnalyserNode | null>(null);
|
||||||
const rangePercent = computed({
|
const rangePercent = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return (elapsedTimeMs.value / durationMs.value) || 0;
|
return (elapsedTimeMs.value / durationMs.value) || 0;
|
||||||
|
@ -262,11 +273,33 @@ const bufferedDataRatio = computed(() => {
|
||||||
function togglePlayPause() {
|
function togglePlayPause() {
|
||||||
if (!isReady.value || !audioEl.value) return;
|
if (!isReady.value || !audioEl.value) return;
|
||||||
|
|
||||||
|
if (!sourceNode.value) {
|
||||||
|
audioContext.value = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
sourceNode.value = audioContext.value.createMediaElementSource(audioEl.value);
|
||||||
|
|
||||||
|
analyserGainNode.value = audioContext.value.createGain();
|
||||||
|
gainNode.value = audioContext.value.createGain();
|
||||||
|
analyserNode.value = audioContext.value.createAnalyser();
|
||||||
|
|
||||||
|
sourceNode.value.connect(analyserGainNode.value);
|
||||||
|
analyserGainNode.value.connect(analyserNode.value);
|
||||||
|
analyserNode.value.connect(gainNode.value);
|
||||||
|
gainNode.value.connect(audioContext.value.destination);
|
||||||
|
|
||||||
|
analyserNode.value.fftSize = 2048;
|
||||||
|
|
||||||
|
analyserGainNode.value.gain.setValueAtTime(0.8, audioContext.value.currentTime);
|
||||||
|
|
||||||
|
gainNode.value.gain.setValueAtTime(volume.value, audioContext.value.currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
if (isPlaying.value) {
|
if (isPlaying.value) {
|
||||||
audioEl.value.pause();
|
audioEl.value.pause();
|
||||||
|
audioVisualizer.value?.pauseAnimation();
|
||||||
isPlaying.value = false;
|
isPlaying.value = false;
|
||||||
} else {
|
} else {
|
||||||
audioEl.value.play();
|
audioEl.value.play();
|
||||||
|
audioVisualizer.value?.resumeAnimation();
|
||||||
isPlaying.value = true;
|
isPlaying.value = true;
|
||||||
oncePlayed.value = true;
|
oncePlayed.value = true;
|
||||||
}
|
}
|
||||||
|
@ -324,6 +357,7 @@ function init() {
|
||||||
oncePlayed.value = false;
|
oncePlayed.value = false;
|
||||||
isActuallyPlaying.value = false;
|
isActuallyPlaying.value = false;
|
||||||
isPlaying.value = false;
|
isPlaying.value = false;
|
||||||
|
audioVisualizer.value?.pauseAnimation();
|
||||||
});
|
});
|
||||||
|
|
||||||
durationMs.value = audioEl.value.duration * 1000;
|
durationMs.value = audioEl.value.duration * 1000;
|
||||||
|
@ -332,8 +366,7 @@ function init() {
|
||||||
durationMs.value = audioEl.value.duration * 1000;
|
durationMs.value = audioEl.value.duration * 1000;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
gainNode.value?.gain.setValueAtTime(volume.value, audioContext.value?.currentTime);
|
||||||
audioEl.value.volume = volume.value;
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
@ -341,7 +374,7 @@ function init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(volume, (to) => {
|
watch(volume, (to) => {
|
||||||
if (audioEl.value) audioEl.value.volume = to;
|
if (audioEl.value) gainNode.value?.gain.setValueAtTime(to, audioContext.value?.currentTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(speed, (to) => {
|
watch(speed, (to) => {
|
||||||
|
|
|
@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
|
<div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="showHiddenContent">
|
||||||
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="showHiddenContent">
|
|
||||||
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
|
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
|
||||||
<b>{{ i18n.ts.sensitive }}</b>
|
<b>{{ i18n.ts.sensitive }}</b>
|
||||||
<span>{{ i18n.ts.clickToShow }}</span>
|
<span>{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<MkMediaAudio v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media" :user="user"/>
|
||||||
<a
|
<a
|
||||||
v-else :class="$style.download"
|
v-else :class="$style.download"
|
||||||
:href="media.url"
|
:href="media.url"
|
||||||
|
@ -33,6 +33,7 @@ import { $i } from '@/account.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
media: Misskey.entities.DriveFile;
|
media: Misskey.entities.DriveFile;
|
||||||
|
user?: Misskey.entities.UserLite;
|
||||||
}>(), {
|
}>(), {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
|
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media" :user="user"/>
|
||||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
|
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
|
||||||
<div
|
<div
|
||||||
ref="gallery"
|
ref="gallery"
|
||||||
|
@ -42,6 +42,7 @@ import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mediaList: Misskey.entities.DriveFile[];
|
mediaList: Misskey.entities.DriveFile[];
|
||||||
|
user?: Misskey.entities.UserLite;
|
||||||
raw?: boolean;
|
raw?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList :mediaList="appearNote.files"/>
|
<MkMediaList :mediaList="appearNote.files" :user="appearNote.user"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
|
|
|
@ -92,7 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList :mediaList="appearNote.files"/>
|
<MkMediaList :mediaList="appearNote.files" :user="appearNote.user"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<details v-if="note.files && note.files.length > 0">
|
<details v-if="note.files && note.files.length > 0">
|
||||||
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
|
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
|
||||||
<MkMediaList :mediaList="note.files"/>
|
<MkMediaList :mediaList="note.files" :user="note.user"/>
|
||||||
</details>
|
</details>
|
||||||
<details v-if="note.poll">
|
<details v-if="note.poll">
|
||||||
<summary>{{ i18n.ts.poll }}</summary>
|
<summary>{{ i18n.ts.poll }}</summary>
|
||||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkA :to="notePage(media.note)">
|
<MkA :to="notePage(media.note)">
|
||||||
<XVideo v-if="media.file.type.startsWith('video')" :key="`video:${media.file.id}`" :class="$style.media" :video="media.file" :videoControls="false"/>
|
<XVideo v-if="media.file.type.startsWith('video')" :key="`video:${media.file.id}`" :class="$style.media" :video="media.file" :videoControls="false"/>
|
||||||
<XImage v-else-if="media.file.type.startsWith('image')" :key="`image:${media.file.id}`" :class="$style.media" class="image" :data-id="media.file.id" :image="media.file" :disableImageLink="true"/>
|
<XImage v-else-if="media.file.type.startsWith('image')" :key="`image:${media.file.id}`" :class="$style.media" class="image" :data-id="media.file.id" :image="media.file" :disableImageLink="true"/>
|
||||||
<XBanner v-else :media="media.file"/>
|
<XBanner v-else :media="media.file" :user="user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="note.files.length > 0" :class="$style.richcontent">
|
<div v-if="note.files.length > 0" :class="$style.richcontent">
|
||||||
<MkMediaList :mediaList="note.files"/>
|
<MkMediaList :mediaList="note.files" :user="note.user"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="note.poll">
|
<div v-if="note.poll">
|
||||||
<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
|
<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
|
||||||
|
|
|
@ -884,6 +884,9 @@ importers:
|
||||||
vuedraggable:
|
vuedraggable:
|
||||||
specifier: next
|
specifier: next
|
||||||
version: 4.1.0(vue@3.5.13(typescript@5.7.2))
|
version: 4.1.0(vue@3.5.13(typescript@5.7.2))
|
||||||
|
webgl-audiovisualizer:
|
||||||
|
specifier: github:tar-bin/webgl-audiovisualizer
|
||||||
|
version: https://codeload.github.com/tar-bin/webgl-audiovisualizer/tar.gz/345b9e82e25a1501749c622790b924658e4914cc
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@misskey-dev/eslint-plugin':
|
'@misskey-dev/eslint-plugin':
|
||||||
specifier: 1.0.0
|
specifier: 1.0.0
|
||||||
|
@ -969,6 +972,9 @@ importers:
|
||||||
'@types/sanitize-html':
|
'@types/sanitize-html':
|
||||||
specifier: 2.13.0
|
specifier: 2.13.0
|
||||||
version: 2.13.0
|
version: 2.13.0
|
||||||
|
'@types/three':
|
||||||
|
specifier: 0.170.0
|
||||||
|
version: 0.170.0
|
||||||
'@types/throttle-debounce':
|
'@types/throttle-debounce':
|
||||||
specifier: 5.0.2
|
specifier: 5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
|
@ -3550,6 +3556,9 @@ packages:
|
||||||
resolution: {integrity: sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==}
|
resolution: {integrity: sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
|
|
||||||
|
'@tweenjs/tween.js@23.1.3':
|
||||||
|
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
||||||
|
|
||||||
'@twemoji/parser@15.0.0':
|
'@twemoji/parser@15.0.0':
|
||||||
resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==}
|
resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==}
|
||||||
|
|
||||||
|
@ -3835,9 +3844,15 @@ packages:
|
||||||
'@types/stack-utils@2.0.3':
|
'@types/stack-utils@2.0.3':
|
||||||
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
||||||
|
|
||||||
|
'@types/stats.js@0.17.3':
|
||||||
|
resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==}
|
||||||
|
|
||||||
'@types/statuses@2.0.5':
|
'@types/statuses@2.0.5':
|
||||||
resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==}
|
resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==}
|
||||||
|
|
||||||
|
'@types/three@0.170.0':
|
||||||
|
resolution: {integrity: sha512-CUm2uckq+zkCY7ZbFpviRttY+6f9fvwm6YqSqPfA5K22s9w7R4VnA3rzJse8kHVvuzLcTx+CjNCs2NYe0QFAyg==}
|
||||||
|
|
||||||
'@types/throttle-debounce@5.0.2':
|
'@types/throttle-debounce@5.0.2':
|
||||||
resolution: {integrity: sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==}
|
resolution: {integrity: sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==}
|
||||||
|
|
||||||
|
@ -3862,6 +3877,9 @@ packages:
|
||||||
'@types/web-push@3.6.4':
|
'@types/web-push@3.6.4':
|
||||||
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
|
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
|
||||||
|
|
||||||
|
'@types/webxr@0.5.20':
|
||||||
|
resolution: {integrity: sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==}
|
||||||
|
|
||||||
'@types/ws@8.5.13':
|
'@types/ws@8.5.13':
|
||||||
resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==}
|
resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==}
|
||||||
|
|
||||||
|
@ -5633,6 +5651,9 @@ packages:
|
||||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||||
engines: {node: ^12.20 || >= 14.13}
|
engines: {node: ^12.20 || >= 14.13}
|
||||||
|
|
||||||
|
fflate@0.8.2:
|
||||||
|
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||||
|
|
||||||
figures@3.2.0:
|
figures@3.2.0:
|
||||||
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
|
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -7018,6 +7039,9 @@ packages:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
meshoptimizer@0.18.1:
|
||||||
|
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
|
||||||
|
|
||||||
methods@1.1.2:
|
methods@1.1.2:
|
||||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -9737,6 +9761,10 @@ packages:
|
||||||
resolution: {integrity: sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==}
|
resolution: {integrity: sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
webgl-audiovisualizer@https://codeload.github.com/tar-bin/webgl-audiovisualizer/tar.gz/345b9e82e25a1501749c622790b924658e4914cc:
|
||||||
|
resolution: {tarball: https://codeload.github.com/tar-bin/webgl-audiovisualizer/tar.gz/345b9e82e25a1501749c622790b924658e4914cc}
|
||||||
|
version: 0.0.0
|
||||||
|
|
||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
|
@ -12958,6 +12986,8 @@ snapshots:
|
||||||
|
|
||||||
'@tsd/typescript@5.4.5': {}
|
'@tsd/typescript@5.4.5': {}
|
||||||
|
|
||||||
|
'@tweenjs/tween.js@23.1.3': {}
|
||||||
|
|
||||||
'@twemoji/parser@15.0.0': {}
|
'@twemoji/parser@15.0.0': {}
|
||||||
|
|
||||||
'@twemoji/parser@15.1.0': {}
|
'@twemoji/parser@15.1.0': {}
|
||||||
|
@ -13253,8 +13283,19 @@ snapshots:
|
||||||
|
|
||||||
'@types/stack-utils@2.0.3': {}
|
'@types/stack-utils@2.0.3': {}
|
||||||
|
|
||||||
|
'@types/stats.js@0.17.3': {}
|
||||||
|
|
||||||
'@types/statuses@2.0.5': {}
|
'@types/statuses@2.0.5': {}
|
||||||
|
|
||||||
|
'@types/three@0.170.0':
|
||||||
|
dependencies:
|
||||||
|
'@tweenjs/tween.js': 23.1.3
|
||||||
|
'@types/stats.js': 0.17.3
|
||||||
|
'@types/webxr': 0.5.20
|
||||||
|
'@webgpu/types': 0.1.38
|
||||||
|
fflate: 0.8.2
|
||||||
|
meshoptimizer: 0.18.1
|
||||||
|
|
||||||
'@types/throttle-debounce@5.0.2': {}
|
'@types/throttle-debounce@5.0.2': {}
|
||||||
|
|
||||||
'@types/tinycolor2@1.4.6': {}
|
'@types/tinycolor2@1.4.6': {}
|
||||||
|
@ -13275,6 +13316,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.10.0
|
'@types/node': 22.10.0
|
||||||
|
|
||||||
|
'@types/webxr@0.5.20': {}
|
||||||
|
|
||||||
'@types/ws@8.5.13':
|
'@types/ws@8.5.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.10.0
|
'@types/node': 22.10.0
|
||||||
|
@ -15540,6 +15583,8 @@ snapshots:
|
||||||
node-domexception: 1.0.0
|
node-domexception: 1.0.0
|
||||||
web-streams-polyfill: 4.0.0
|
web-streams-polyfill: 4.0.0
|
||||||
|
|
||||||
|
fflate@0.8.2: {}
|
||||||
|
|
||||||
figures@3.2.0:
|
figures@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 1.0.5
|
escape-string-regexp: 1.0.5
|
||||||
|
@ -17228,6 +17273,8 @@ snapshots:
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
|
meshoptimizer@0.18.1: {}
|
||||||
|
|
||||||
methods@1.1.2: {}
|
methods@1.1.2: {}
|
||||||
|
|
||||||
mfm-js@0.24.0:
|
mfm-js@0.24.0:
|
||||||
|
@ -17735,7 +17782,7 @@ snapshots:
|
||||||
nodemon@3.1.7:
|
nodemon@3.1.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 4.0.1
|
chokidar: 4.0.1
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
ignore-by-default: 1.0.1
|
ignore-by-default: 1.0.1
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
pstree.remy: 1.1.8
|
pstree.remy: 1.1.8
|
||||||
|
@ -20152,6 +20199,8 @@ snapshots:
|
||||||
|
|
||||||
web-streams-polyfill@4.0.0: {}
|
web-streams-polyfill@4.0.0: {}
|
||||||
|
|
||||||
|
webgl-audiovisualizer@https://codeload.github.com/tar-bin/webgl-audiovisualizer/tar.gz/345b9e82e25a1501749c622790b924658e4914cc: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
webidl-conversions@7.0.0: {}
|
webidl-conversions@7.0.0: {}
|
||||||
|
|
Loading…
Reference in a new issue