[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.
This commit is contained in:
Rekushi authored and GitHub committed 2026-09-15 23:22:31 +01:00
1 parent b6d610d15f
commit 4817f99974
3 files changed
+213 -3

No files matched your search

+182
View File
@@ -0,0 +1,182 @@
(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.');
});
}());
+5
View File
@@ -81,3 +81,8 @@ input,textarea,select{width:100%;border:1px solid var(--line);border-radius:3px;
@media(max-width:1050px){.flipnote-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.watch-layout{grid-template-columns:minmax(0,1fr) 270px}.docs-layout{grid-template-columns:205px minmax(0,1fr)}}
@media(max-width:820px){.credit-list{grid-template-columns:1fr}.docs-layout{grid-template-columns:1fr}.docs-nav{position:static}.docs-nav nav{grid-template-columns:repeat(3,minmax(0,1fr))}.watch-head{align-items:flex-start}.related-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.header-inner{height:auto;min-height:68px;flex-wrap:wrap;gap:0 12px;padding-top:6px;padding-bottom:6px}.brand{width:164px;height:54px}.brand img{width:164px}.main-nav{order:3;width:100%;height:36px}.nav-link{padding:0 10px}.header-actions{margin-left:auto}.flipnote-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.watch-layout{grid-template-columns:1fr}.detail-panel{position:static}.comment{grid-template-columns:120px 1fr}.admin-ban-form{grid-template-columns:1fr}.page-shell{padding-top:20px}}
@media(max-width:580px){.docs-nav nav{grid-template-columns:repeat(2,minmax(0,1fr))}.watch-head{flex-direction:column;align-items:stretch}.preview-stage{min-height:220px}.preview-footer{align-items:flex-start;flex-direction:column}.related-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.header-inner{padding-left:12px;padding-right:12px}.brand{width:150px}.brand img{width:150px}.nav-action{padding:0 8px;font-size:12px}.nav-action:not(.ghost){display:none}.page-shell{padding:16px 12px 50px}.page-head,.page-head.split,.home-intro-card,.site-footer{align-items:flex-start;flex-direction:column}.page-head h1{font-size:25px}.tabs{width:100%;justify-content:space-between}.flipnote-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.comment{grid-template-columns:1fr;gap:4px}.comment-form{align-items:stretch;flex-direction:column}.site-footer{padding-left:12px;padding-right:12px}.site-footer>div{flex-wrap:wrap}}
/* flipnote.js web player */
.flipnote-stage{position:relative;overflow:hidden;min-height:0;aspect-ratio:4/3}.flipnote-player-canvas{display:none;width:100%;height:100%;align-items:center;justify-content:center;background:#080708;cursor:pointer}.flipnote-stage.is-ready .flipnote-player-canvas{display:flex}.flipnote-player-canvas canvas{display:block;width:100%!important;height:100%!important;max-width:100%;object-fit:contain;background:#080708;image-rendering:auto}.flipnote-player-fallback{display:block;width:100%;height:100%;object-fit:contain;background:#080708}.flipnote-stage.is-ready .flipnote-player-fallback,.flipnote-player-fallback[hidden]{display:none}.flipnote-player-status{position:absolute;left:12px;bottom:12px;padding:5px 8px;border:1px solid rgba(255,255,255,.12);border-radius:3px;background:rgba(8,7,8,.84);color:#d7cfcd;font-size:11px;font-weight:700}.flipnote-big-play{position:absolute;left:50%;top:50%;width:64px;height:64px;transform:translate(-50%,-50%);border:1px solid rgba(255,255,255,.28);border-radius:50%;background:rgba(8,7,8,.78);color:#fff;font:700 25px/1 system-ui,sans-serif;cursor:pointer;padding:0 0 1px 4px;box-shadow:0 3px 18px rgba(0,0,0,.35)}.flipnote-big-play:hover{background:rgba(177,59,41,.9)}.flipnote-big-play[hidden]{display:none}.flipnote-stage.has-player-error .flipnote-player-status{border-color:#70423b;background:rgba(33,21,19,.94)}
.flipnote-controls{display:grid;grid-template-columns:auto minmax(110px,1fr) auto auto auto;gap:8px;align-items:center;padding:8px 10px;border-top:1px solid var(--line);background:#111012}.flipnote-control-button{min-width:58px;height:30px;padding:0 9px;border:1px solid var(--line);border-radius:3px;background:var(--surface-raised);color:#ddd5d3;font:700 11px/1 system-ui,sans-serif;cursor:pointer}.flipnote-control-button:hover:not(:disabled),.flipnote-control-button.is-active{border-color:#7e463e;background:#2a1916;color:#fff}.flipnote-control-button:disabled{opacity:.45;cursor:default}.flipnote-progress{width:100%;min-width:70px;accent-color:var(--accent);cursor:pointer}.flipnote-progress:disabled{cursor:default;opacity:.45}.flipnote-time{min-width:82px;color:var(--muted);font:700 10px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;text-align:center;white-space:nowrap}
@media(max-width:580px){.flipnote-controls{grid-template-columns:auto minmax(90px,1fr) auto}.flipnote-time{grid-column:2;text-align:left}.flipnote-control-button[data-action="mute"]{grid-column:3}.flipnote-control-button[data-action="loop"]{grid-column:3}.flipnote-big-play{width:56px;height:56px;font-size:22px}}