Dynamic loading, error handling, and types

This commit is contained in:
CenTdemeern1 2024-10-16 01:01:17 +02:00
parent 8d1d09e42d
commit 24bc2cc19a

View file

@ -19,17 +19,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<span>Always be wary of arbitrary code execution!</span> <span>Always be wary of arbitrary code execution!</span>
<span>{{ i18n.ts.clickToShow }}</span> <span>{{ i18n.ts.clickToShow }}</span>
</div> </div>
<div v-if="ruffleError" class="player-hide">
<b><i class="ph-warning ph-bold ph-lg"></i> Flash Content Failed To Load:</b>
<code>{{ ruffleError }}</code>
</div>
<div v-else-if="loadingStatus" class="player-hide">
<b>Flash Content Is Loading<MkEllipsis/></b>
<MkLoading/>
<p>{{ loadingStatus }}</p>
</div>
<div ref="ruffleContainer" class="container"></div> <div ref="ruffleContainer" class="container"></div>
</div> </div>
<div class="controls"> <div class="controls">
<button class="play" @click="playPause()"> <button class="play" @click="playPause()">
<i v-if="player.value?.isPlaying" class="ph-pause ph-bold ph-lg"></i> <!-- FIXME: Broken? --> <i v-if="player?.isPlaying" class="ph-pause ph-bold ph-lg"></i> <!-- FIXME: Broken? (Though less so than before) -->
<i v-else class="ph-play ph-bold ph-lg"></i> <i v-else class="ph-play ph-bold ph-lg"></i>
</button> </button>
<button class="stop" @click="stop()"> <button class="stop" @click="stop()">
<i class="ph-stop ph-bold ph-lg"></i> <i class="ph-stop ph-bold ph-lg"></i>
</button> </button>
<input v-model="player.volume" type="range" min="0" max="1" step="0.1"/> <input v-if="player" v-model="player.volume" type="range" min="0" max="1" step="0.1"/>
<input v-else type="range" min="0" max="1" value="1" disabled/>
<a class="download" :title="i18n.ts.download" :href="flashFile.url" target="_blank"> <a class="download" :title="i18n.ts.download" :href="flashFile.url" target="_blank">
<i class="ph-download ph-bold ph-lg"></i> <i class="ph-download ph-bold ph-lg"></i>
</a> </a>
@ -43,12 +53,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, nextTick, computed, watch, onDeactivated, onMounted } from 'vue'; import { ref, computed, onDeactivated } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import packageInfo from '../../package.json'; import packageInfo from '../../package.json';
import MkEllipsis from './global/MkEllipsis.vue';
import MkLoading from './global/MkLoading.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import '@ruffle-rs/ruffle'; import { PublicAPI, PublicAPILike } from '@/types/ruffle/setup'; // This gives us the types for window.RufflePlayer, etc via side effects
import { PlayerElement } from '@/types/ruffle/player';
const props = defineProps<{ const props = defineProps<{
flashFile: Misskey.entities.DriveFile flashFile: Misskey.entities.DriveFile
@ -60,15 +73,38 @@ const comment = computed(() => { return props.flashFile.comment ?? ''; });
let hide = ref((defaultStore.state.nsfw === 'force') ? true : isSensitive.value && (defaultStore.state.nsfw !== 'ignore')); let hide = ref((defaultStore.state.nsfw === 'force') ? true : isSensitive.value && (defaultStore.state.nsfw !== 'ignore'));
let playerHide = ref(true); let playerHide = ref(true);
let ruffleContainer = ref<HTMLDivElement>(); let ruffleContainer = ref<HTMLDivElement>();
let loadingStatus = ref<string | undefined>(undefined);
let player = ref<PlayerElement | undefined>(undefined);
let ruffleError = ref<string | undefined>(undefined);
declare global { function dismissWarning() {
interface Window { playerHide.value = false;
RufflePlayer: any; loadRuffle().then(() => {
try {
createPlayer();
loadContent();
} catch (error) {
handleError(error);
} }
}).catch(handleError);
} }
window.RufflePlayer = window.RufflePlayer || {}; function handleError(error: unknown) {
window.RufflePlayer.config = { if (error instanceof Error) ruffleError.value = error.stack;
else ruffleError.value = `${error}`; // Fallback for if something is horribly wrong
}
/**
* @throws if unpkg shits itself
*/
async function loadRuffle() {
if (window.RufflePlayer !== undefined) return;
loadingStatus.value = 'Loading Ruffle player';
await import('@ruffle-rs/ruffle'); // Assumption: this will throw if unpkg has a hiccup or something. If not, the next undefined check will catch it.
window.RufflePlayer = window.RufflePlayer as PublicAPILike | PublicAPI | undefined; // Assert unknown type due to side effects
if (window.RufflePlayer === undefined) throw Error('unpkg has shit itself, but not in an expected way (has unpkg permanently shut down? how close is the heat death of the universe?)');
window.RufflePlayer.config = {
// Options affecting the whole page // Options affecting the whole page
'publicPath': `https://unpkg.com/@ruffle-rs/ruffle@${packageInfo.dependencies['@ruffle-rs/ruffle']}/`, 'publicPath': `https://unpkg.com/@ruffle-rs/ruffle@${packageInfo.dependencies['@ruffle-rs/ruffle']}/`,
'polyfills': false, 'polyfills': false,
@ -81,8 +117,8 @@ window.RufflePlayer.config = {
'wmode': 'window', 'wmode': 'window',
'letterbox': 'on', 'letterbox': 'on',
'warnOnUnsupportedContent': true, 'warnOnUnsupportedContent': true,
'contextMenu': 'off', 'contextMenu': 'off', // Prevent two overlapping context menus. Most of the stuff in this context menu is available in the controls below the player.
'showSwfDownload': false, 'showSwfDownload': false, // Handled by custom download button
'upgradeToHttps': window.location.protocol === 'https:', 'upgradeToHttps': window.location.protocol === 'https:',
'maxExecutionDuration': 15, 'maxExecutionDuration': 15,
'logLevel': 'error', 'logLevel': 'error',
@ -105,21 +141,37 @@ window.RufflePlayer.config = {
'credentialAllowList': [], 'credentialAllowList': [],
'playerRuntime': 'flashPlayer', 'playerRuntime': 'flashPlayer',
'allowFullscreen': false, // Handled by custom fullscreen button 'allowFullscreen': false, // Handled by custom fullscreen button
}; };
const ruffle = window.RufflePlayer.newest();
const player = ref(ruffle.createPlayer());
player.value.style.width = '100%';
player.value.style.height = '100%';
function dismissWarning() {
playerHide.value = false;
loadContent();
} }
/**
* @throws If `ruffle.newest()` fails (impossible)
*/
function createPlayer() {
if (player.value !== undefined) return;
const ruffle = (() => {
const ruffleAPI = (window.RufflePlayer as PublicAPI).newest();
if (ruffleAPI === null) {
// This error exists because non-null assertions are forbidden, apparently.
throw Error('Ruffle could not get the latest Ruffle source. Since we\'re loading from unpkg this is genuinely impossible and you must\'ve done something incredibly cursed.');
}
return ruffleAPI;
})();
player.value = ruffle.createPlayer();
player.value.style.width = '100%';
player.value.style.height = '100%';
}
/**
* @throws If `player.value` is uninitialized.
*/
function loadContent() { function loadContent() {
if (player.value === undefined) throw Error('Player is uninitialized.');
ruffleContainer.value?.appendChild(player.value); ruffleContainer.value?.appendChild(player.value);
player.value.load(url.value).catch((error) => { loadingStatus.value = 'Loading Flash file';
player.value.load(url.value).then(() => {
loadingStatus.value = undefined;
}).catch((error) => {
console.error(error); console.error(error);
}); });
} }
@ -129,6 +181,7 @@ function playPause() {
dismissWarning(); dismissWarning();
return; return;
} }
if (player.value === undefined) return; // Not done loading or something
if (player.value.isPlaying) { if (player.value.isPlaying) {
player.value.pause(); player.value.pause();
} else { } else {
@ -137,6 +190,7 @@ function playPause() {
} }
function fullscreen() { function fullscreen() {
if (player.value === undefined) return; // Can't fullscreen an element that doesn't exist.
if (player.value.isFullscreen) { if (player.value.isFullscreen) {
player.value.exitFullscreen(); player.value.exitFullscreen();
} else { } else {
@ -145,6 +199,7 @@ function fullscreen() {
} }
function stop() { function stop() {
if (player.value === undefined) return; // FIXME: This doesn't stop the loading process. (Though, should this even be implemented?)
try { try {
ruffleContainer.value?.removeChild(player.value); ruffleContainer.value?.removeChild(player.value);
} catch { } catch {
@ -176,6 +231,7 @@ onDeactivated(() => {
.height-hack { .height-hack {
/* HACK: I'm too stupid to do this better apparently. Copy-pasted from MkMediaList.vue */ /* HACK: I'm too stupid to do this better apparently. Copy-pasted from MkMediaList.vue */
/* height: 100% doesn't work */ /* height: 100% doesn't work */
/* FIXME: This breaks with more than one attachment, and the controls start overlapping the note buttons (like, boost, reply, etc) */
height: clamp( height: clamp(
64px, 64px,
50cqh, 50cqh,
@ -294,6 +350,10 @@ onDeactivated(() => {
margin: 4px 8px; margin: 4px 8px;
overflow-x: hidden; overflow-x: hidden;
&:disabled {
filter: grayscale(100%);
}
&:focus { &:focus {
outline: none; outline: none;