[TikiWiki-commits] [Git][tikiwiki/tiki][master] [NEW] Tracker Field Files: add an audio player component to play all audio...
Benoit Grégoire (@benoitg) via TikiWiki-cvs <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <689119661d2eb_2c9ae75089417@gitlab-sidekiq-low-urgency-cpu-bound-v2-5cdb66b9cb-j4gx4.mail> |
Benoit Grégoire pushed to branch master at Tiki Wiki CMS Groupware / Tiki
Commits:
87d066fb by Merci Jacob at 2025-08-04T20:06:58+00:00
[NEW] Tracker Field Files: add an audio player component to play all audio files(especially live recorded ones) from the tracker item interface
---
* add general error message
* keep validation message for corrupted files and bad network
* move the inline audio player to a separate smarty function
* update error validation message clearly stating unsupported formats
* fix playing audio from the admin file gallery interface
* allow pausing and stopping of the playing track and improve the look
* [NEW] Tracker Field Files: add an audio player component to play all audio files(especially live recorded ones) from the tracker item interface
See merge request tikiwiki/tiki!7991
- - - - -
10 changed files:
- lib/core/Services/RecordRtc/Controller.php
- lib/core/Tracker/Field/Files.php
- lib/smarty_tiki/Extension/SmartyTikiExtension.php
- + lib/smarty_tiki/FunctionHandler/InlineAudioPlayer.php
- lib/smarty_tiki/Modifier/Iconify.php
- + lib/smarty_tiki/function.inline_audio_player.php
- path_js_importmap_generator.php
- + src/js/jquery-tiki/tracker-fields/files.js
- templates/list_file_gallery_content.tpl
- themes/base_files/scss/_tiki-trackers.scss
Changes:
=====================================
lib/core/Services/RecordRtc/Controller.php
=====================================
@@ -32,10 +32,12 @@ class Services_RecordRtc_Controller
$fileName = $audioFilename;
$tempName = $_FILES['audioblob']['tmp_name'];
$_FILES['data'] = $_FILES['audioblob'];
+ $_FILES['data']['type'] = 'audio/webm';
} else {
$fileName = $videoFilename;
$tempName = $_FILES['videoblob']['tmp_name'];
$_FILES['data'] = $_FILES['videoblob'];
+ $_FILES['data']['type'] = 'video/webm';
}
if (empty($fileName) || empty($tempName)) {
@@ -63,7 +65,6 @@ class Services_RecordRtc_Controller
return;
}
$_FILES['data']['name'] = $fileName;
- $_FILES['data']['type'] = ($extension == 'webm') ? 'video/webm' : $_FILES['data']['type'];
if ($galleryId) {
$_FILES['data']['galleryId'] = $galleryId;
=====================================
lib/core/Tracker/Field/Files.php
=====================================
@@ -598,11 +598,13 @@ class Tracker_Field_Files extends \Tracker\Field\AbstractItemField implements \T
$ret .= '<li>';
if ($prefs['vimeo_upload'] == 'y' && $this->getOption('displayMode') == 'vimeo') {
$ret .= smarty_function_icon(['name' => 'vimeo'], $smarty->getEmptyInternalTemplate());
+ } elseif (substr($file['filetype'], 0, 6) == 'audio/') {
+ $ret .= smarty_function_inline_audio_player(['fileId' => $fileId], $smarty->getEmptyInternalTemplate());
} else {
$ret .= smarty_modifier_iconify('tiki-download_file.php?fileId=' . $fileId, $file['filetype'], $fileId, 2);
}
- $ret .= smarty_function_object_link(['type' => 'file', 'id' => $fileId, 'title' => $file['name']], $smarty->getEmptyInternalTemplate());
+ $ret .= smarty_function_object_link(['type' => 'file', 'id' => $fileId, 'title' => $file['name'], 'class' => 'label'], $smarty->getEmptyInternalTemplate());
$globalperms = Perms::get([ 'type' => 'file gallery', 'object' => $galleryId ]);
@@ -650,7 +652,7 @@ class Tracker_Field_Files extends \Tracker\Field\AbstractItemField implements \T
'trigger' => 'click'
], $smarty->getEmptyInternalTemplate());
$icon = smarty_function_icon(['name' => 'wrench'], $smarty->getEmptyInternalTemplate());
- $ret .= " <a class='fgalname tips' title='" . tr('Actions') . "'href='#' $popup>$icon</a>";
+ $ret .= " <a class='fgalname tips label' title='" . tr('Actions') . "'href='#' $popup>$icon</a>";
}
$ret .= '</li>';
=====================================
lib/smarty_tiki/Extension/SmartyTikiExtension.php
=====================================
@@ -454,6 +454,9 @@ class SmartyTikiExtension extends \Smarty\Extension\Base
case 'icon':
$this->functionHandlers[$functionName] = new \SmartyTiki\FunctionHandler\Icon();
break;
+ case 'inline_audio_player':
+ $this->functionHandlers[$functionName] = new \SmartyTiki\FunctionHandler\InlineAudioPlayer();
+ break;
case 'initials_filter_links':
$this->functionHandlers[$functionName] = new \SmartyTiki\FunctionHandler\InitialsFilterLinks();
break;
=====================================
lib/smarty_tiki/FunctionHandler/InlineAudioPlayer.php
=====================================
@@ -0,0 +1,40 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+
+namespace SmartyTiki\FunctionHandler;
+
+use TikiLib;
+
+class InlineAudioPlayer extends \Smarty\FunctionHandler\Base
+{
+ public function handle($params, \Smarty\Template $template)
+ {
+ TikiLib::lib('header')->add_js_module('import "@jquery-tiki/tracker-fields/files";');
+ $icon = smarty_function_icon(['name' => 'play'], $template);
+ $pauseIcon = smarty_function_icon(['name' => 'pause', 'size' => 1], $template);
+ $stopIcon = smarty_function_icon(['name' => 'xmark', 'size' => 1], $template);
+ $src = 'tiki-download_file.php?fileId=' . $params['fileId'];
+
+ return <<<HTML
+ <button class="play-audio btn btn-sm btn-link bg-secondary-subtle rounded-circle" data-src="{$src}">
+ {$icon}
+ </button>
+ <button class="pause-audio btn btn-sm btn-link bg-secondary-subtle rounded-circle d-none">
+ {$pauseIcon}
+ </button>
+ <div class="audio-timer">
+ <div class="end-line rounded-pill bg-secondary-subtle">
+ <div class="progress-bar rounded-pill bg-secondary"></div>
+ </div>
+ <div class="time d-none fw-lighter"></div>
+ </div>
+ <button class="stop-audio btn btn-sm btn-link rounded-circle d-none">
+ {$stopIcon}
+ </button>
+ HTML;
+ }
+}
=====================================
lib/smarty_tiki/Modifier/Iconify.php
=====================================
@@ -7,6 +7,8 @@
namespace SmartyTiki\Modifier;
+use TikiLib;
+
/**
* Smarty plugin
* -------------------------------------------------------------
=====================================
lib/smarty_tiki/function.inline_audio_player.php
=====================================
@@ -0,0 +1,12 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+
+function smarty_function_inline_audio_player($params, \Smarty\Template $template)
+{
+ $smartyFunctionInlineAudioPlayerHandler = new \SmartyTiki\FunctionHandler\InlineAudioPlayer();
+ return $smartyFunctionInlineAudioPlayerHandler->handle($params, $template);
+}
=====================================
path_js_importmap_generator.php
=====================================
@@ -73,6 +73,7 @@ function generateJsImportmapScripts(bool $useBaseUrl = false)
"@jquery-tiki/tiki-password" => $tikiUrl . JS_ASSETS_PATH . "/jquery-tiki/tiki-password.js",
"@jquery-tiki/timeago" => $tikiUrl . JS_ASSETS_PATH . "/jquery-tiki/timeago.js",
"@jquery-tiki/tracker-fields/emailFolder" => $tikiUrl . JS_ASSETS_PATH . "/jquery-tiki/tracker-fields/emailFolder.js",
+ "@jquery-tiki/tracker-fields/files" => $tikiUrl . JS_ASSETS_PATH . "/jquery-tiki/tracker-fields/files.js",
"@tiki-modules/sentryBrowser" => $tikiUrl . JS_ASSETS_PATH . "/tiki-sentry-browser.js",
"@mermaidPack" => $tikiUrl . JS_ASSETS_PATH . "/tiki-mermaid.js",
"@tiki-glightbox" => $tikiUrl . JS_ASSETS_PATH . "/tiki-glightbox.js",
=====================================
src/js/jquery-tiki/tracker-fields/files.js
=====================================
@@ -0,0 +1,133 @@
+import moment from "moment";
+
+$(".play-audio").on("click", async function () {
+ $(this).addClass("loading");
+ $(this).prop("disabled", true);
+
+ try {
+ const audio = await getPlayableAudio.call(this);
+
+ $(this).removeClass("loading");
+ $(this).prop("disabled", false);
+
+ if (!audio) {
+ showMessage(tr("Failed to load audio file."), "error");
+ return;
+ }
+
+ await audio.play();
+
+ $(this).addClass("d-none");
+ $(this).siblings(".pause-audio").removeClass("d-none").data("audio", audio);
+ $(this).siblings(".stop-audio").removeClass("d-none").data("audio", audio);
+
+ const audioTimer = $(this).siblings(".audio-timer");
+ audioTimer.addClass("show");
+ audioTimer.find(".time").removeClass("d-none");
+ $(this).siblings(".label").hide();
+
+ audio.addEventListener("timeupdate", () => {
+ const currentTime = Math.floor(audio.currentTime);
+ const duration = Math.floor(audio.duration);
+ const remainder = duration - currentTime;
+
+ const now = moment().minute(0).second(0).unix();
+ const endTime = now + remainder;
+
+ const minutes = Math.floor(remainder / 60);
+ let timeLabel = "";
+ if (minutes > 60) {
+ timeLabel = moment.unix(endTime).format("HH:mm:ss");
+ } else {
+ timeLabel = moment.unix(endTime).format("mm:ss");
+ }
+
+ audioTimer.find(".time").text(timeLabel);
+
+ const percentage = (currentTime / duration) * 100;
+ audioTimer.find(".progress-bar").css("width", `${percentage}%`);
+ });
+ audio.addEventListener("ended", () => {
+ audioTimer.removeClass("show");
+ $(this).siblings(".label").show();
+ audioTimer.find(".time").addClass("d-none");
+ audioTimer.find(".progress-bar").css("width", "0%");
+
+ $(this).removeClass("d-none");
+ $(this).siblings(".pause-audio").addClass("d-none");
+ $(this).siblings(".stop-audio").addClass("d-none");
+ });
+ } catch (event) {
+ const errorCode = event.target?.error?.code;
+ if ([MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, MediaError.MEDIA_ERR_DECODE].includes(errorCode)) {
+ showMessage(tr("Failed to play audio, the format is unsupported."), "error");
+ } else {
+ showMessage(tr("Failed to play audio: ") + event.target?.error?.message, "error");
+ }
+ $(this).removeClass("loading");
+ $(this).prop("disabled", false);
+ }
+});
+
+$(".pause-audio").on("click", function () {
+ const audio = $(this).data("audio");
+ if (audio) {
+ audio.pause();
+ $(this).addClass("d-none");
+ $(this).siblings(".play-audio").removeClass("d-none").data("audio", audio);
+ }
+});
+
+$(".stop-audio").on("click", function () {
+ const audio = $(this).data("audio");
+ if (audio) {
+ audio.pause();
+ audio.currentTime = 0;
+
+ audio.dispatchEvent(new Event("ended"));
+ }
+});
+
+function getPlayableAudio() {
+ return new Promise(async (resolve, reject) => {
+ const audio = $(this).data("audio");
+ if (audio) {
+ resolve(audio);
+ } else {
+ const url = $(this).data("src");
+ fetch(url)
+ .then(async (response) => {
+ if (!response.ok) {
+ resolve(null);
+ }
+ const blob = await response.blob();
+ const audio = new Audio(URL.createObjectURL(blob));
+ loadMetadata(audio)
+ .then(() => resolve(audio))
+ .catch(reject);
+ })
+ .catch(() => resolve(null));
+ }
+ });
+}
+
+function loadMetadata(audioElement) {
+ return new Promise((resolve, reject) => {
+ audioElement.addEventListener("loadedmetadata", () => {
+ if (audioElement.duration === Infinity) {
+ audioElement.currentTime = 1e101; // Set a large value to force metadata loading
+
+ audioElement.ontimeupdate = () => {
+ audioElement.ontimeupdate = null;
+ audioElement.currentTime = 0;
+ resolve();
+ };
+ } else {
+ resolve();
+ }
+ });
+ audioElement.addEventListener("error", (e) => {
+ reject(e);
+ });
+ });
+}
=====================================
templates/list_file_gallery_content.tpl
=====================================
@@ -413,7 +413,7 @@
{if isset($files[changes].isgal) && $files[changes].isgal eq 1}
{capture assign=propval}{icon name='file-archive-open' class=''}{/capture}
{else}
- {$propval=$files[changes].filename|iconify:$files[changes].type}
+ {$propval=$files[changes].filename|iconify:$files[changes].type:$files[changes].id}
{/if}
{elseif $propname eq 'description' and $gal_info.max_desc gt 0}
{$propval=$propval|truncate:$gal_info.max_desc:"...":false|nl2br}
=====================================
themes/base_files/scss/_tiki-trackers.scss
=====================================
@@ -26,7 +26,7 @@
padding-right: 1.5em;
margin-bottom: 1em;
font-size: 0.9em;
- img, span { margin-right: 6px; }
+ & > img, > span, > button { margin-right: 6px; }
}
@media (min-width: 768px) {
@@ -39,6 +39,60 @@
.tracker-item-files {
padding-left: 15px;
+
+ li {
+ display: flex !important;
+ align-items: center;
+ gap: 0.5rem;
+ }
+}
+
+.play-audio.loading {
+ position: relative;
+}
+
+.play-audio.loading::after {
+ content: '';
+ position: absolute;
+ top: 0%;
+ left: 0%;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ border: 2px solid transparent;
+ border-top-color: var(--bs-primary);
+ animation: spin 1s ease-out infinite;
+}
+
+.audio-timer {
+ width: 0%;
+ position: relative;
+ transition: width 0.5s ease;
+ margin-left: -0.5rem;
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+
+ .end-line {
+ width: 100%;
+ height: 5px;
+
+ .progress-bar {
+ width: 0%;
+ height: 100%;
+ transition: width 0.5s ease;
+ }
+ }
+}
+
+.audio-timer.show {
+ width: 50%;
+ margin-left: 0;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
}
#tracker_selector {
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/87d066fb4d825ef61677510f45a0407fa492684a
--
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/87d066fb4d825ef61677510f45a0407fa492684a
You're receiving this email because of your account on gitlab.com.
_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs