diff --git a/web/static/flipnote-player.js b/web/static/flipnote-player.js new file mode 100644 index 0000000..fb76e02 --- /dev/null +++ b/web/static/flipnote-player.js @@ -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.'); + }); +}()); diff --git a/web/static/site.css b/web/static/site.css index 22db9ad..5fbd72e 100644 --- a/web/static/site.css +++ b/web/static/site.css @@ -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}} diff --git a/webapp.py b/webapp.py index 78c1506..a9d2dc8 100644 --- a/webapp.py +++ b/webapp.py @@ -119,6 +119,7 @@ class WebRoot(resource.Resource): '
The original open-source Flipnote Hatena replacement server and the main foundation for the older public server code.
Utilities for PPM/TMB, UGO, NTFT and related Nintendo DSi and Flipnote formats. The bundled copy has been ported to Python 3.
Browser-side PPM decoding, animation rendering and audio playback on Flipnote pages.
The language and runtime used by RexiMemo OSE.
The networking and HTTP framework used by the server.
Used by the legacy Flipnote media and format tools.
by %(creator_name)s · %(posted)s