diff options
| author | ilotterytea <iltsu@alright.party> | 2025-06-15 16:14:16 +0400 |
|---|---|---|
| committer | ilotterytea <iltsu@alright.party> | 2025-06-15 16:14:16 +0400 |
| commit | 648c126b070fc83ddb66b0353d03c4ad31b87551 (patch) | |
| tree | 3a83989f5eac1ef21e58d06fb293e14b2be636b4 | |
| parent | d152925126af0d311746518a20f6a409869ae02d (diff) | |
feat: audio recording
| -rw-r--r-- | public/index.php | 12 | ||||
| -rwxr-xr-x | public/static/img/icons/pause.png | bin | 0 -> 721 bytes | |||
| -rwxr-xr-x | public/static/img/icons/play.png | bin | 0 -> 717 bytes | |||
| -rwxr-xr-x | public/static/img/icons/repeat.png | bin | 0 -> 750 bytes | |||
| -rw-r--r-- | public/static/scripts/audiorecorder.js | 210 | ||||
| -rw-r--r-- | public/static/style.css | 11 |
6 files changed, 230 insertions, 3 deletions
diff --git a/public/index.php b/public/index.php index 9e6ccf1..c3eea01 100644 --- a/public/index.php +++ b/public/index.php @@ -233,7 +233,7 @@ $privacy_exists = is_file($_SERVER['DOCUMENT_ROOT'] . '/static/PRIVACY.txt'); </section> <section class="box column"> - <div class="tabs"> + <div class="tabs" id="form-upload-tabs"> <div class="form-upload-tab tab" id="form-tab-file"> <button onclick="showUploadType('file')" class="transparent"> <p>File Upload</p> @@ -288,6 +288,8 @@ $privacy_exists = is_file($_SERVER['DOCUMENT_ROOT'] . '/static/PRIVACY.txt'); <textarea name="paste" placeholder="Enter your text here..."></textarea> </div> + <div class="column" id="form-record-upload" style="display: none;"></div> + <table class="vertical left" id="form-upload-options"> <tr> <th>Title:</th> @@ -336,6 +338,7 @@ $privacy_exists = is_file($_SERVER['DOCUMENT_ROOT'] . '/static/PRIVACY.txt'); } </script> <?php elseif (!$file): ?> + <script src="/static/scripts/audiorecorder.js"></script> <script> const formDetails = document.getElementById('form-upload-options'); @@ -591,8 +594,13 @@ $privacy_exists = is_file($_SERVER['DOCUMENT_ROOT'] . '/static/PRIVACY.txt'); } function showUploadType(type) { - document.getElementById('form-upload-wrapper').style.display = type == 'text' ? 'none' : 'flex'; + if (document.getElementById('form-upload-tabs').hasAttribute('disabled')) { + return; + } + + document.getElementById('form-upload-wrapper').style.display = type == 'file' ? 'flex' : 'none'; document.getElementById('form-text-upload').style.display = type == 'text' ? 'flex' : 'none'; + document.getElementById('form-record-upload').style.display = type === 'audio' ? 'flex' : 'none'; const tabs = document.querySelectorAll('.form-upload-tab'); diff --git a/public/static/img/icons/pause.png b/public/static/img/icons/pause.png Binary files differnew file mode 100755 index 0000000..ec61099 --- /dev/null +++ b/public/static/img/icons/pause.png diff --git a/public/static/img/icons/play.png b/public/static/img/icons/play.png Binary files differnew file mode 100755 index 0000000..f8c8ec6 --- /dev/null +++ b/public/static/img/icons/play.png diff --git a/public/static/img/icons/repeat.png b/public/static/img/icons/repeat.png Binary files differnew file mode 100755 index 0000000..406ec33 --- /dev/null +++ b/public/static/img/icons/repeat.png diff --git a/public/static/scripts/audiorecorder.js b/public/static/scripts/audiorecorder.js new file mode 100644 index 0000000..b12a2af --- /dev/null +++ b/public/static/scripts/audiorecorder.js @@ -0,0 +1,210 @@ +// getting form upload tabs +const tabs = document.getElementById('form-upload-tabs'); + +// creating an audio button +tabs.innerHTML += ` +<div class="form-upload-tab tab disabled" id="form-tab-audio"> + <button onclick="showUploadType('audio')" class="transparent"> + <p>Record</p> + </button> +</div> +`; + +function generateAudioRecorderHTML() { + return ` + <div class="column gap-8"> + <button type="button" class="big-upload-button" onclick="startRecording()" id="record-start-btn"> + <h1>Click here to start audio recording</h1> + </button> + <button type="button" class="big-upload-button" onclick="stopRecording()" id="record-stop-btn" style="display: none"> + <h1>Recording...</h1> + <p>Click here to stop audio recording</p> + </button> + <div class="row align-center justify-center"> + <div class="box row align-center gap-8 pad-8" id="record-player" style="display:none"> + <button type="button" onclick="removeRecording()"> + <img src="/static/img/icons/cross.png" alt="Delete"> + </button> + <button type="button" onclick="startRecording()"> + <img src="/static/img/icons/repeat.png" alt="Retry"> + </button> + <button type="button" onclick="playRecord()" id="record-play-btn"> + <img src="/static/img/icons/play.png" alt="Play"> + </button> + <button type="button" onclick="pauseRecord()" id="record-pause-btn" style="display: none;"> + <img src="/static/img/icons/pause.png" alt="Pause"> + </button> + <div class="column gap-8"> + <input type="range" min="0" max="100" value="0" step="0.01" class="audio-slider" id="record-slider" oninput="rewindRecord()"> + <div class="row justify-between"> + <p id="record-currentsecond"></p> + <p id="record-duration"></p> + </div> + </div> + </div> + </div> + </div> +`; +} + +const form = document.getElementById('form-record-upload'); +form.innerHTML = generateAudioRecorderHTML(); + +const startBtn = document.getElementById('record-start-btn'); +const stopBtn = document.getElementById('record-stop-btn'); +const playBtn = document.getElementById('record-play-btn'); +const pauseBtn = document.getElementById('record-pause-btn'); + +const player = document.getElementById('record-player'); +const slider = document.getElementById('record-slider'); +const currentSecond = document.getElementById('record-currentsecond'); +const duration = document.getElementById('record-duration'); + +let playback = null; + +// record functions +let mediaRecorder; +let stream; +let audioChunks = []; +let audioLength = 0; + +async function startRecording() { + // TODO: very poor sound quality + stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 44100, channelCount: 1 }}); + const options = { mimeType: 'audio/ogg; codecs=opus', audioBitsPerSecond: 128000 }; + if (!MediaRecorder.isTypeSupported(options.mimeType)) { + alert("Your browser doesn't support audio/ogg recording."); + return; + } + + audioLength = 0; + + mediaRecorder = new MediaRecorder(stream, options); + audioChunks = []; + + mediaRecorder.ondataavailable = event => { + if (event.data.size > 0) { + audioChunks.push(event.data); + audioLength++; + } + }; + + mediaRecorder.start(); + startBtn.style.display = 'none'; + stopBtn.style.display = 'block'; + player.style.display = 'none'; + + setFormDetailsVisiblity(false); +} + +function stopRecording() { + mediaRecorder.stop(); + startBtn.style.display = 'block'; + stopBtn.style.display = 'none'; + + if (playback) { + form.removeChild(playback); + playback = null; + } + + mediaRecorder.onstop = () => { + const blob = new Blob(audioChunks, { type: 'audio/ogg; codecs=opus' }); + const file = new File([blob], 'recording.ogg', { type: 'audio/ogg; codecs=opus' }); + + const url = URL.createObjectURL(file); + + playback = document.createElement('audio'); + playback.src = url; + + playback.addEventListener('loadedmetadata', () => { + const d = playback.duration; + slider.max = d; + slider.value = 0; + currentSecond.textContent = formatTimestamp(slider.getAttribute('value')); + duration.textContent = formatTimestamp(d); + }); + + playback.addEventListener('timeupdate', () => { + currentSecond.textContent = formatTimestamp(playback.currentTime); + slider.value = playback.currentTime; + }); + + playback.addEventListener('ended', () => { + playBtn.style.display = 'flex'; + pauseBtn.style.display = 'none'; + currentSecond.textContent = formatTimestamp(0); + slider.value = 0; + }); + + form.appendChild(playback); + + playBtn.style.display = 'flex'; + pauseBtn.style.display = 'none'; + + startBtn.style.display = 'none'; + stopBtn.style.display = 'none'; + player.style.display = 'flex'; + + setFormDetailsVisiblity(true); + + stream.getAudioTracks().forEach(track => { + track.stop(); + }); + + stream = null; + + // attaching the file + if (formFile) { + const dt = new DataTransfer(); + dt.items.add(file); + formFile.files = dt.files; + } + + tabs.setAttribute('disabled', 'true'); + }; +} + +function removeRecording() { + startBtn.style.display = 'block'; + player.style.display = 'none'; + form.removeChild(playback); + formFile.value = ''; + setFormDetailsVisiblity(false); + + tabs.removeAttribute('disabled'); +} + +function playRecord() { + if (playback) playback.play(); + + playBtn.style.display = 'none'; + pauseBtn.style.display = 'flex'; +} + +function pauseRecord() { + if (playback) playback.pause(); + + playBtn.style.display = 'flex'; + pauseBtn.style.display = 'none'; +} + +function rewindRecord() { + currentSecond.textContent = formatTimestamp(slider.value); + if (playback) { + playback.currentTime = slider.value; + playRecord(); + } +} + +function formatTimestamp(timestamp) { + const m = Math.floor(timestamp / 60); + const s = Math.ceil(timestamp % 60); + + return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s +} + +// form +document.getElementById('form-upload').addEventListener('submit', () => { + player.style.display = 'none'; + startBtn.style.display = 'block'; +});
\ No newline at end of file diff --git a/public/static/style.css b/public/static/style.css index e6dd5fd..63e5409 100644 --- a/public/static/style.css +++ b/public/static/style.css @@ -136,7 +136,8 @@ button[type=submit]:hover { cursor: pointer; } -#form-upload-wrapper>button { +#form-upload-wrapper>button, +.big-upload-button { background: var(--background-2); padding: 32px 0; color: var(--foreground); @@ -278,6 +279,10 @@ button[type=submit]:hover { justify-content: end; } +.justify-between { + justify-content: space-between; +} + .align-center { align-items: center; } @@ -290,6 +295,10 @@ button[type=submit]:hover { padding: 4px; } +.pad-8 { + padding: 8px; +} + .gap-16 { gap: 16px; } |
