[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.