Files
RexiMemo-OSE/web/static/flipnote-player.js
T
Rekushi 4817f99974 [FEATURE] Add flipnote.js web playback
Adds in-browser Flipnote playback with audio, seeking, mute and looping using flipnote.js. Introduces a separate media route for playback so loading a Flipnote does not increment download counts, while retaining the existing first-frame preview as a fallback.
2026-09-15 23:22:31 +01:00

183 lines
5.1 KiB
JavaScript

(function () {
'use strict';
var stage = document.querySelector('[data-flipnote-player]');
if (!stage) return;
var source = stage.getAttribute('data-source');
var mount = stage.querySelector('.flipnote-player-canvas');
var fallback = stage.querySelector('.flipnote-player-fallback');
var status = stage.querySelector('[data-display="status"]');
var bigPlay = stage.querySelector('[data-action="big-play"]');
var controls = stage.parentNode.querySelector('.flipnote-controls');
var playButton = controls.querySelector('[data-action="play"]');
var muteButton = controls.querySelector('[data-action="mute"]');
var loopButton = controls.querySelector('[data-action="loop"]');
var progress = controls.querySelector('[data-control="progress"]');
var time = controls.querySelector('[data-display="time"]');
var controlButtons = controls.querySelectorAll('button');
var seeking = false;
var hasPlayed = false;
var player;
function setControlsEnabled(enabled) {
for (var i = 0; i < controlButtons.length; i++) {
controlButtons[i].disabled = !enabled;
}
progress.disabled = !enabled;
}
function setPlayState(playing) {
playButton.textContent = playing ? 'Pause' : 'Play';
bigPlay.hidden = playing;
stage.classList.toggle('is-playing', playing);
if (playing) hasPlayed = true;
}
function updateTime() {
if (!player || !player.isNoteLoaded) return;
time.textContent = player.getTimeCounter();
}
function updateProgress(value) {
if (seeking) return;
var amount = Number(value);
if (!isFinite(amount)) amount = player && player.isNoteLoaded ? player.getProgress() : 0;
progress.value = String(Math.max(0, Math.min(1000, Math.round(amount * 10))));
updateTime();
}
function updateVolume() {
if (!player || !player.isNoteLoaded) return;
var muted = player.getMuted();
muteButton.textContent = muted ? 'Muted' : 'Sound';
muteButton.setAttribute('aria-pressed', muted ? 'true' : 'false');
}
function updateLoop() {
if (!player || !player.isNoteLoaded) return;
var looped = player.getLoop();
loopButton.classList.toggle('is-active', looped);
loopButton.setAttribute('aria-pressed', looped ? 'true' : 'false');
}
function showError(message) {
stage.classList.add('has-player-error');
stage.classList.remove('is-ready');
fallback.hidden = false;
bigPlay.hidden = true;
status.hidden = false;
status.textContent = message;
setControlsEnabled(false);
}
if (!window.flipnote || !window.flipnote.Player) {
showError('Playback unavailable — showing the first frame.');
return;
}
try {
player = new window.flipnote.Player(mount, 320, 240);
} catch (err) {
showError('Playback is not supported by this browser.');
return;
}
player.on('ready', function () {
stage.classList.add('is-ready');
stage.classList.remove('has-player-error');
fallback.hidden = true;
status.hidden = true;
bigPlay.hidden = false;
setControlsEnabled(true);
setPlayState(false);
updateProgress(0);
updateVolume();
updateLoop();
});
player.on('play', function () {
setPlayState(true);
});
player.on('pause', function () {
setPlayState(false);
});
player.on('ended', function () {
setPlayState(false);
updateProgress(100);
});
player.on('progress', function (value) {
updateProgress(value);
});
player.on('timeupdate', function () {
updateTime();
});
player.on('volumechange', function () {
updateVolume();
});
player.on('error', function () {
showError('Could not play this Flipnote — showing the first frame.');
});
function togglePlay() {
if (!player || !player.isNoteLoaded) return;
player.togglePlay();
}
playButton.addEventListener('click', togglePlay);
bigPlay.addEventListener('click', togglePlay);
mount.addEventListener('click', function () {
if (hasPlayed) togglePlay();
});
muteButton.addEventListener('click', function () {
if (!player || !player.isNoteLoaded) return;
player.toggleMuted();
updateVolume();
});
loopButton.addEventListener('click', function () {
if (!player || !player.isNoteLoaded) return;
player.toggleLoop();
updateLoop();
});
function beginSeek() {
if (!player || !player.isNoteLoaded || seeking) return;
seeking = true;
player.startSeek();
}
function moveSeek() {
if (!player || !player.isNoteLoaded) return;
if (!seeking) beginSeek();
player.seek(Number(progress.value) / 1000);
updateTime();
}
function finishSeek() {
if (!player || !player.isNoteLoaded || !seeking) return;
player.seek(Number(progress.value) / 1000);
player.endSeek();
seeking = false;
updateProgress(player.getProgress());
}
progress.addEventListener('pointerdown', beginSeek);
progress.addEventListener('input', moveSeek);
progress.addEventListener('change', finishSeek);
progress.addEventListener('pointerup', finishSeek);
progress.addEventListener('keyup', finishSeek);
player.load(source).catch(function () {
showError('Could not play this Flipnote — showing the first frame.');
});
}());