Plugin Directory

Changeset 3495969


Ignore:
Timestamp:
03/31/2026 07:50:53 PM (5 days ago)
Author:
mvirik
Message:

Release 3.1.4

Location:
text-to-speech-tts/trunk
Files:
2 deleted
9 edited

Legend:

Unmodified
Added
Removed
  • text-to-speech-tts/trunk/admin/partials/pages/advanced.php

    r3493900 r3495969  
    2323    // Data Management
    2424    update_option('mementor_tts_delete_data', isset($_POST['mementor_tts_delete_data']) ? '1' : '0');
    25 
    26     // Telemetry consent
    27     $telemetry_consent = isset($_POST['mementor_tts_telemetry_consent']) ? 'yes' : 'no';
    28     update_option('mementor_tts_telemetry_consent', $telemetry_consent);
    29     update_option('mementor_tts_telemetry_consent_time', time());
    3025
    3126    // Custom CSS
     
    114109                        <label class="tts-toggle">
    115110                            <input type="checkbox" name="mementor_tts_delete_data" value="1" <?php checked(get_option('mementor_tts_delete_data', '1'), '1'); ?>>
    116                             <span class="tts-toggle-slider"></span>
    117                         </label>
    118                     </div>
    119 
    120                     <div class="tts-setting-row">
    121                         <div>
    122                             <div class="tts-setting-label"><?php esc_html_e('Usage Statistics', 'text-to-speech-tts'); ?></div>
    123                             <div class="tts-setting-hint"><?php esc_html_e('Share anonymous usage data (feature counts, plugin version, WP version). No personal data collected.', 'text-to-speech-tts'); ?></div>
    124                         </div>
    125                         <label class="tts-toggle">
    126                             <input type="checkbox" name="mementor_tts_telemetry_consent" value="1" <?php checked(get_option('mementor_tts_telemetry_consent', ''), 'yes'); ?>>
    127111                            <span class="tts-toggle-slider"></span>
    128112                        </label>
  • text-to-speech-tts/trunk/admin/partials/pages/player.php

    r3493900 r3495969  
    3030$show_download = get_option('mementor_tts_show_download', false);
    3131$show_info = get_option('mementor_tts_show_info', false);
    32 
    33 // Check if show_info is forced by remote config
    34 $show_info_remote_override = Mementor_Text_To_Speech::get_remote_config_override('mementor_tts_show_info');
    35 $show_info_forced = ($show_info_remote_override !== null);
    36 if ($show_info_forced) {
    37     $show_info = $show_info_remote_override;
    38 }
    3932
    4033// Check if API key is set
     
    291284                    <div class="tts-control-item">
    292285                        <span class="tts-control-label"><?php esc_html_e('Information', 'text-to-speech-tts'); ?></span>
    293                         <div class="mementor-tts-toggle"><input type="checkbox" id="mementor_tts_show_info" name="mementor_tts_show_info" value="1" <?php checked($show_info); ?> <?php echo $show_info_forced ? 'disabled' : ''; ?>><label for="mementor_tts_show_info"><span class="slider"></span></label></div>
     286                        <div class="mementor-tts-toggle"><input type="checkbox" id="mementor_tts_show_info" name="mementor_tts_show_info" value="1" <?php checked($show_info); ?> ><label for="mementor_tts_show_info"><span class="slider"></span></label></div>
    294287                    </div>
    295288                    <div class="tts-control-item">
  • text-to-speech-tts/trunk/includes/class-mementor-tts-analytics.php

    r3493900 r3495969  
    650650    }
    651651   
    652     /**
    653      * Export analytics data for remote sync
    654      *
    655      * @param string $period Period to export
    656      * @return array
    657      */
    658     public function export_analytics_for_sync($period = 'day') {
    659         global $wpdb;
    660        
    661         $date_condition = $this->get_date_condition($period);
    662        
    663         // Get aggregated data
    664         $data = $wpdb->get_results(
    665             "SELECT
    666                 domain_hash,
    667                 user_type,
    668                 COUNT(*) as requests,
    669                 SUM(credits_used) as credits,
    670                 plugin_version,
    671                 pro_version,
    672                 country_code,
    673                 DATE(created_at) as date
    674              FROM {$this->table_name}
    675              WHERE {$date_condition}
    676              GROUP BY domain_hash, user_type, DATE(created_at)"
    677         );
    678        
    679         // Add metadata
    680         $export = array(
    681             'site_hash' => $this->get_domain_info()['hash'],
    682             'export_date' => current_time('mysql'),
    683             'period' => $period,
    684             'data' => $data,
    685             'plugin_version' => MEMENTOR_TTS_VERSION,
    686             'php_version' => PHP_VERSION,
    687             'wp_version' => get_bloginfo('version')
    688         );
    689        
    690         // Sign the export
    691         $export['signature'] = $this->integrity_helper->generate_signature($export, 'analytics_export');
    692        
    693         return $export;
    694     }
    695    
    696     /**
    697      * Send analytics to remote server
    698      * This runs periodically to sync data
    699      */
    700     public function send_analytics_to_remote() {
    701         // Check if remote analytics is enabled
    702         if (!get_option('mementor_tts_enable_remote_analytics', true)) {
    703             return;
    704         }
    705        
    706         // Get last sync time
    707         $last_sync = get_option('mementor_tts_last_analytics_sync', 0);
    708         $now = time();
    709        
    710         // Only sync once per day
    711         if ($now - $last_sync < DAY_IN_SECONDS) {
    712             return;
    713         }
    714        
    715         // Export data for the last day
    716         $export_data = $this->export_analytics_for_sync('day');
    717        
    718         // Send to remote endpoint
    719         $response = wp_remote_post('https://api.mementor.no/v1/analytics', array(
    720             'timeout' => 30,
    721             'headers' => array(
    722                 'Content-Type' => 'application/json',
    723                 'X-Plugin-Version' => MEMENTOR_TTS_VERSION
    724             ),
    725             'body' => wp_json_encode($export_data)
    726         ));
    727        
    728         if (!is_wp_error($response)) {
    729             $response_code = wp_remote_retrieve_response_code($response);
    730             if ($response_code === 200) {
    731                 // Update last sync time
    732                 update_option('mementor_tts_last_analytics_sync', $now, false);
    733                
    734                 // Clean up local data after successful sync
    735                 $this->cleanup_synced_data();
    736             }
    737         }
    738     }
    739    
    740     /**
    741      * Clean up synced data to save space
    742      */
    743     private function cleanup_synced_data() {
    744         global $wpdb;
    745        
    746         // Keep only last 7 days of detailed data locally
    747         $cutoff = date('Y-m-d H:i:s', strtotime('-7 days'));
    748        
    749         $wpdb->query($wpdb->prepare(
    750             "DELETE FROM {$this->table_name} WHERE created_at < %s",
    751             $cutoff
    752         ));
    753     }
    754652}
  • text-to-speech-tts/trunk/includes/class-mementor-tts-elevenlabs-api.php

    r3493900 r3495969  
    8383     * Shared key manager instance
    8484     */
    85     private $shared_key_manager = null;
    8685   
    87     /**
    88      * Remote telemetry instance
    89      */
    90     private $telemetry = null;
    9186
    9287    /**
     
    132127        // Initialize debug mode
    133128        $this->debug_mode = get_option('mementor_tts_debug_mode', '0') === '1';
    134        
    135         // Shared key manager removed — synthesis now goes through SaaS API client
    136         $this->shared_key_manager = null;
    137        
    138         // Initialize telemetry
    139         if (!class_exists('Mementor_TTS_Remote_Telemetry')) {
    140             require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-remote-telemetry.php';
    141         }
    142         $this->telemetry = Mementor_TTS_Remote_Telemetry::get_instance();
    143129       
    144130        // Get the API key from options
     
    10481034                }
    10491035               
    1050                 // Track API error in telemetry
    1051                 if ($this->telemetry) {
    1052                     $this->telemetry->track_usage('error', array(
    1053                         'error_code' => 'api_' . $status_code,
    1054                         'error_message' => $friendly_message
    1055                     ));
    1056                 }
    1057                
    10581036                return array(
    10591037                    'success' => false,
     
    11101088                $remaining_credits = $user_credits->get_remaining_credits();
    11111089               
    1112                 // Log usage to shared key manager for analytics
    1113                 if ($this->shared_key_manager) {
    1114                     $this->shared_key_manager->log_shared_key_usage($credits_used, 'text_to_speech');
    1115                 }
    11161090            }
    11171091           
     
    11341108            }
    11351109           
    1136             // Track telemetry
    1137             if ($this->telemetry) {
    1138                 $telemetry_data = array(
    1139                     'text_length' => strlen($text),
    1140                     'using_shared_key' => $used_shared_key
    1141                 );
    1142                 $this->telemetry->track_usage('text_to_speech', $telemetry_data);
    1143             }
    1144 
    11451110            return $result;
    11461111           
    11471112        } catch (Exception $e) {
    11481113            $this->log_message('Exception in text_to_speech: ' . $e->getMessage(), 'error');
    1149            
    1150             // Track error in telemetry
    1151             if ($this->telemetry) {
    1152                 $this->telemetry->track_usage('error', array(
    1153                     'error_code' => 'exception',
    1154                     'error_message' => $e->getMessage()
    1155                 ));
    1156             }
    11571114           
    11581115            return array(
     
    17131670    /**
    17141671     * Get decrypted shared API key
    1715      * 
     1672     *
    17161673     * @return string|null Decrypted API key or null
    17171674     */
    17181675    private function get_decrypted_shared_key() {
    1719         if (!$this->shared_key_manager) {
    1720             return null;
    1721         }
    1722        
    1723         return $this->shared_key_manager->get_shared_key();
     1676        return null;
    17241677    }
    17251678   
  • text-to-speech-tts/trunk/includes/class-mementor-tts-public.php

    r3493900 r3495969  
    711711
    712712        // Process content using CSS selectors if processor is available
     713        if (!class_exists('Mementor_TTS_Processor')) {
     714            require_once MEMENTOR_TTS_PLUGIN_DIR . 'includes/class-mementor-tts-processor.php';
     715        }
    713716        $processor = Mementor_TTS_Processor::get_instance();
    714717        if ($processor && method_exists($processor, 'get_content_from_html')) {
  • text-to-speech-tts/trunk/includes/class-mementor-tts.php

    r3494985 r3495969  
    5555        add_action('plugins_loaded', array($this, 'init_elementor_integration'));
    5656
    57         // Initialize remote config for domain-specific setting overrides
    58         add_action('init', array($this, 'init_remote_config'));
    59     }
    60 
    61     /**
    62      * Initialize remote config system for domain-specific setting overrides.
    63      * Settings are fetched from the CRM and cached locally.
    64      */
    65     public function init_remote_config() {
    66         // Only apply overrides on frontend
    67         if (is_admin()) {
    68             return;
    69         }
    70 
    71         $overrides = $this->get_remote_config_overrides();
    72 
    73         if (!empty($overrides) && is_array($overrides)) {
    74             foreach ($overrides as $option_name => $value) {
    75                 add_filter('pre_option_' . $option_name, function($pre_option) use ($value) {
    76                     return $value;
    77                 }, 10, 1);
    78             }
    79         }
    80     }
    81 
    82     /**
    83      * Get remote config overrides for the current domain.
    84      * Results are cached for 12 hours.
    85      *
    86      * @return array Associative array of option_name => forced_value
    87      */
    88     public function get_remote_config_overrides() {
    89         $cache_key = 'mementor_tts_remote_config';
    90         $cached = get_transient($cache_key);
    91 
    92         if (false !== $cached) {
    93             return $cached;
    94         }
    95 
    96         // Get current domain
    97         $domain = $this->get_current_domain();
    98 
    99         // Generate signature for authentication
    100         $timestamp = time();
    101         $secret = 'mementor_tts_config_v1';
    102         $signature = hash_hmac('sha256', $domain . $timestamp, $secret);
    103 
    104         // Fetch from CRM
    105         $response = wp_remote_get(
    106             'https://crm.mementor.no/plugin/api/telemetry/v1/settings.php?domain=' . urlencode($domain),
    107             array(
    108                 'timeout' => 5,
    109                 'headers' => array(
    110                     'X-Plugin-Version' => MEMENTOR_TTS_VERSION,
    111                     'X-TTS-Timestamp' => $timestamp,
    112                     'X-TTS-Signature' => $signature,
    113                 ),
    114             )
    115         );
    116 
    117         $overrides = array();
    118 
    119         if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
    120             $body = wp_remote_retrieve_body($response);
    121             $data = json_decode($body, true);
    122 
    123             if (isset($data['overrides']) && is_array($data['overrides'])) {
    124                 $overrides = $data['overrides'];
    125             }
    126         }
    127 
    128         // Cache for 12 hours (even if empty, to avoid repeated requests)
    129         set_transient($cache_key, $overrides, 12 * HOUR_IN_SECONDS);
    130 
    131         return $overrides;
    132     }
    133 
    134     /**
    135      * Get the current domain without www prefix.
    136      *
    137      * @return string
    138      */
    139     public function get_current_domain() {
    140         $host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
    141         return preg_replace('/^www\./', '', $host);
    142     }
    143 
    144     /**
    145      * Check if a specific option has a remote config override.
    146      *
    147      * @param string $option_name The option name to check.
    148      * @return mixed|null The override value if set, null otherwise.
    149      */
    150     public function get_remote_override($option_name) {
    151         $overrides = $this->get_remote_config_overrides();
    152         return isset($overrides[$option_name]) ? $overrides[$option_name] : null;
    153     }
    154 
    155     /**
    156      * Static helper to check for remote config override.
    157      * Can be called without a class instance.
    158      *
    159      * @param string $option_name The option name to check.
    160      * @return mixed|null The override value if set, null otherwise.
    161      */
    162     public static function get_remote_config_override($option_name) {
    163         $cache_key = 'mementor_tts_remote_config';
    164         $cached = get_transient($cache_key);
    165 
    166         // If not cached, we need to fetch - create temporary instance logic
    167         if (false === $cached) {
    168             // Get current domain
    169             $host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
    170             $domain = preg_replace('/^www\./', '', $host);
    171 
    172             // Generate signature for authentication
    173             $timestamp = time();
    174             $secret = 'mementor_tts_config_v1';
    175             $signature = hash_hmac('sha256', $domain . $timestamp, $secret);
    176 
    177             // Fetch from CRM
    178             $response = wp_remote_get(
    179                 'https://crm.mementor.no/plugin/api/telemetry/v1/settings.php?domain=' . urlencode($domain),
    180                 array(
    181                     'timeout' => 5,
    182                     'headers' => array(
    183                         'X-Plugin-Version' => MEMENTOR_TTS_VERSION,
    184                         'X-TTS-Timestamp' => $timestamp,
    185                         'X-TTS-Signature' => $signature,
    186                     ),
    187                 )
    188             );
    189 
    190             $overrides = array();
    191 
    192             if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
    193                 $body = wp_remote_retrieve_body($response);
    194                 $data = json_decode($body, true);
    195 
    196                 if (isset($data['overrides']) && is_array($data['overrides'])) {
    197                     $overrides = $data['overrides'];
    198                 }
    199             }
    200 
    201             // Cache for 12 hours
    202             set_transient($cache_key, $overrides, 12 * HOUR_IN_SECONDS);
    203             $cached = $overrides;
    204         }
    205 
    206         return isset($cached[$option_name]) ? $cached[$option_name] : null;
    20757    }
    20858
  • text-to-speech-tts/trunk/public/js/mementor-tts-player-insert.js

    r3476321 r3495969  
    3636    };
    3737
    38     // Log initialization if debug is enabled
    39     safeDebug('Player insertion script initialized');
    40     safeDebug('Player insertion using PHP hooks - JavaScript insertion disabled');
    41 
    42     // IMPORTANT: Player insertion is now handled by PHP hooks for better compatibility
     38    // Player insertion is handled by PHP hooks for better compatibility
    4339    // This script is kept for backward compatibility but does not insert players
    44     return;
    45    
    46     // Don't skip auto-insertion just because there are shortcode players
    47     // Both types can coexist on the same page
    48    
    49     // Store references to existing shortcode players to ensure they're not affected
    50     const existingShortcodePlayers = $('.mementor-tts-player-container[data-player-type="shortcode"]');
    51     safeDebug('Found ' + existingShortcodePlayers.length + ' existing shortcode players to preserve');
    52 
    53     // Check if we have the required data
    54     if (typeof mementorTTSPlayerHTML === 'undefined' || !mementorTTSPlayerHTML.html) {
    55         // This is expected on pages with shortcodes, so just log debug info
    56         safeDebug('Player HTML data is not available - this is normal for shortcode usage');
    57         return;
    58     }
    59 
    60     // Get player position from localized data
    61     var position = mementorTTSPlayerPosition.position;
    62     safeDebug('Player position -', position);
    63    
    64     // Function to create the player label with proper classes
    65     function createPlayerLabel() {
    66         // Check if label settings exist and if the label should be shown
    67         if (!mementorTTSPlayerLabel || !mementorTTSPlayerLabel.show_label) {
    68             return '';
    69         }
    70 
    71         const labelClasses = ['mementor-tts-player-label'];
    72 
    73         // Add text alignment class
    74         if (mementorTTSPlayerLabel.text_align) {
    75             labelClasses.push(`text-${mementorTTSPlayerLabel.text_align}`);
    76         }
    77 
    78         // Add position class
    79         if (mementorTTSPlayerPosition) {
    80             labelClasses.push(`mementor-tts-${mementorTTSPlayerPosition}`);
    81         }
    82 
    83         // Add font size class
    84         const fontSize = parseInt(mementorTTSPlayerLabel.font_size);
    85         if (fontSize <= 12) {
    86             labelClasses.push('label-size-small');
    87         } else if (fontSize >= 16) {
    88             labelClasses.push('label-size-large');
    89         } else {
    90             labelClasses.push('label-size-default');
    91         }
    92 
    93         // Add font weight class
    94         const fontWeight = parseInt(mementorTTSPlayerLabel.font_weight);
    95         if (fontWeight >= 700) {
    96             labelClasses.push('label-weight-bold');
    97         } else if (fontWeight >= 500) {
    98             labelClasses.push('label-weight-medium');
    99         } else {
    100             labelClasses.push('label-weight-normal');
    101         }
    102 
    103         // Add color class
    104         if (mementorTTSPlayerLabel.color) {
    105             const color = mementorTTSPlayerLabel.color.toLowerCase();
    106             if (color === '#ffffff' || color === 'white') {
    107                 labelClasses.push('label-color-white');
    108             } else if (color === '#1d2327' || color === 'dark') {
    109                 labelClasses.push('label-color-dark');
    110             } else {
    111                 // For custom colors, use a simpler approach with just 'custom' class
    112                 labelClasses.push('label-color-custom');
    113             }
    114         } else {
    115             labelClasses.push('label-color-dark'); // Default color
    116         }
    117 
    118         // Use the labelText property from mementorTTSData for the label content
    119         const labelText = typeof mementorTTSData !== 'undefined' && mementorTTSData.labelText
    120             ? mementorTTSData.labelText
    121             : mementorTTSPlayerLabel.text || 'Listen to this article:';
    122 
    123         // For custom colors, include the color in the style attribute
    124         if (mementorTTSPlayerLabel.color &&
    125             mementorTTSPlayerLabel.color.toLowerCase() !== '#ffffff' &&
    126             mementorTTSPlayerLabel.color.toLowerCase() !== 'white' &&
    127             mementorTTSPlayerLabel.color.toLowerCase() !== '#1d2327' &&
    128             mementorTTSPlayerLabel.color.toLowerCase() !== 'dark') {
    129             return `<span class="${labelClasses.join(' ')}" style="--mementor-tts-custom-label-color: ${mementorTTSPlayerLabel.color}">${labelText}</span>`;
    130         } else {
    131             return `<span class="${labelClasses.join(' ')}">${labelText}</span>`;
    132         }
    133     }
    134 
    135     // Get player HTML from localized data
    136     var playerHTML = mementorTTSPlayerHTML.html;
    137 
    138     // Replace the default label with our dynamically styled one
    139     if (typeof mementorTTSPlayerLabel !== 'undefined' && mementorTTSPlayerLabel.show) {
    140         var labelHTML = createPlayerLabel();
    141         // Replace the existing label div with our new one
    142         var tempDiv = document.createElement('div');
    143         tempDiv.innerHTML = playerHTML;
    144         var existingLabel = tempDiv.querySelector('.mementor-tts-player-label');
    145         if (existingLabel) {
    146             existingLabel.outerHTML = labelHTML;
    147         } else {
    148             // If no label exists, insert before the player
    149             var player = tempDiv.querySelector('.mementor-tts-player');
    150             if (player) {
    151                 player.insertAdjacentHTML('beforebegin', labelHTML);
    152             }
    153         }
    154         playerHTML = tempDiv.innerHTML;
    155     }
    156 
    157     // Create a position-specific class
    158     var positionClass = 'mementor-tts-' + position;
    159    
    160     // Use jQuery to create a DOM element we can modify
    161     var $tempContainer = $('<div>').html(playerHTML);
    162     var $playerContainer = $tempContainer.find('.mementor-tts-player-container');
    163    
    164     // Reset classes to the base container class to avoid duplications
    165     var baseClasses = ['mementor-tts-player-container'];
    166    
    167     // Add position class
    168     baseClasses.push(positionClass);
    169    
    170     // Add alignment class if specified
    171     if (typeof mementorTTSPlayerAlignment !== 'undefined') {
    172         var alignmentClass = 'mementor-tts-align-' + mementorTTSPlayerAlignment.alignment;
    173         baseClasses.push(alignmentClass);
    174         // Store alignment as data attribute
    175         $playerContainer.attr('data-alignment', mementorTTSPlayerAlignment.alignment);
    176     } else {
    177         // Use default alignment (left) if not specified
    178         baseClasses.push('mementor-tts-align-left');
    179         $playerContainer.attr('data-alignment', 'left');
    180     }
    181    
    182     // Add width class if specified
    183     if (typeof mementorTTSPlayerWidth !== 'undefined') {
    184         var widthClass = 'mementor-tts-width-' + mementorTTSPlayerWidth.width;
    185         baseClasses.push(widthClass);
    186        
    187         // Add max-width class if needed
    188         if (mementorTTSPlayerWidth.maxWidth) {
    189             baseClasses.push('mementor-tts-max-width-custom');
    190             $playerContainer.attr('data-max-width', mementorTTSPlayerWidth.maxWidth);
    191             $playerContainer.attr('data-max-width-unit', mementorTTSPlayerWidth.maxWidthUnit || '%');
    192         }
    193     } else {
    194         // Use default width (content_width) if not specified
    195         baseClasses.push('mementor-tts-width-content_width');
    196     }
    197    
    198     // Set the classes instead of adding to existing ones to avoid duplications
    199     $playerContainer.attr('class', baseClasses.join(' '));
    200    
    201     // Add data attribute to identify this as an auto-inserted player
    202     $playerContainer.attr('data-player-type', 'auto-inserted');
    203    
    204     // Add post ID if available
    205     if (typeof mementorTTS !== 'undefined' && mementorTTS.postId) {
    206         $playerContainer.attr('data-post-id', mementorTTS.postId);
    207     } else if (typeof mementorTTSStats !== 'undefined' && mementorTTSStats.postId) {
    208         $playerContainer.attr('data-post-id', mementorTTSStats.postId);
    209     }
    210    
    211     // Get the updated HTML
    212     playerHTML = $tempContainer.html();
    213 
    214     // Function to initialize the newly inserted player
    215     function initializeInsertedPlayer($player) {
    216         var audio = $player.find('audio')[0];
    217         var durationDisplay = $player.find('.mementor-tts-duration-display')[0];
    218        
    219         if (audio && durationDisplay) {
    220             // Force preload metadata
    221             audio.preload = "metadata";
    222            
    223             // Load the audio
    224             audio.load();
    225            
    226             // Add metadata loaded listener
    227             audio.addEventListener('loadedmetadata', function() {
    228                 if (audio.duration && !isNaN(audio.duration)) {
    229                     var minutes = Math.floor(audio.duration / 60);
    230                     var seconds = Math.floor(audio.duration % 60);
    231                     durationDisplay.textContent = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
    232                     safeDebug('Player duration loaded:', durationDisplay.textContent);
    233                 }
    234             });
    235            
    236             // Fallback: Check duration periodically
    237             var attempts = 0;
    238             var maxAttempts = 10;
    239             var checkInterval = setInterval(function() {
    240                 if (attempts >= maxAttempts) {
    241                     clearInterval(checkInterval);
    242                     return;
    243                 }
    244                
    245                 if (audio.duration && !isNaN(audio.duration)) {
    246                     var minutes = Math.floor(audio.duration / 60);
    247                     var seconds = Math.floor(audio.duration % 60);
    248                     durationDisplay.textContent = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
    249                     clearInterval(checkInterval);
    250                 }
    251                
    252                 attempts++;
    253             }, 500);
    254         }
    255     }
    256    
    257     // Find the appropriate insertion point based on position
    258     var inserted = false;
    259    
    260     switch (position) {
    261         case 'before_title':
    262             var titleSelectors = [
    263                 'h1.entry-title',
    264                 '.entry-header h1',
    265                 '.post-title',
    266                 'article h1:first-of-type',
    267                 '.elementor-widget-theme-post-title h1',
    268                 '.page-title',
    269                 '.site-content h1:first-of-type',
    270                 '.wp-block-post-title',
    271                 'h1.header-title' // Added for Uncode theme support
    272             ];
    273            
    274             // Try each selector
    275             $.each(titleSelectors, function(index, selector) {
    276                 var titleElement = $(selector).first();
    277                 if (titleElement.length) {
    278                     // Add specific classes for position and context
    279                     var $player = $(playerHTML)
    280                         .addClass('mementor-tts-before-title-js')
    281                         .addClass('mementor-tts-adjacent-to-' + selector.replace(/[^a-zA-Z0-9]/g, '-'));
    282                     titleElement.before($player);
    283                     initializeInsertedPlayer($player);
    284                     inserted = true;
    285                     safeDebug('Player inserted before title using selector:', selector);
    286                     return false; // Break the loop
    287                 }
    288             });
    289             break;
    290            
    291         case 'after_title':
    292             var titleSelectors = [
    293                 'h1.entry-title',
    294                 '.entry-header h1',
    295                 '.post-title',
    296                 'article h1:first-of-type',
    297                 '.elementor-widget-theme-post-title h1',
    298                 '.page-title',
    299                 '.site-content h1:first-of-type',
    300                 '.wp-block-post-title',
    301                 'h1.header-title' // Added for Uncode theme support
    302             ];
    303            
    304             // Try each selector
    305             $.each(titleSelectors, function(index, selector) {
    306                 var titleElement = $(selector).first();
    307                 if (titleElement.length) {
    308                     var $player = $(playerHTML)
    309                         .addClass('mementor-tts-after-title-js')
    310                         .addClass('mementor-tts-adjacent-to-' + selector.replace(/[^a-zA-Z0-9]/g, '-'));
    311                     titleElement.after($player);
    312                     initializeInsertedPlayer($player);
    313                     inserted = true;
    314                     safeDebug('Player inserted after title using selector:', selector);
    315                     return false; // Break the loop
    316                 }
    317             });
    318             break;
    319            
    320     case 'after_title_before_excerpt':
    321         var titleSelectors = [
    322             '.entry-header',
    323             'h1.entry-title',
    324             '.entry-header h1',
    325             '.post-title',
    326             'article h1:first-of-type',
    327             '.elementor-widget-theme-post-title h1',
    328             '.page-title',
    329             '.site-content h1:first-of-type',
    330             '.wp-block-post-title',
    331             'h1.header-title' // Added for Uncode theme support
    332         ];
    333            
    334         // Try each selector
    335         $.each(titleSelectors, function(index, selector) {
    336             var titleElement = $(selector).first();
    337             if (titleElement.length) {
    338                 // Find the excerpt or content element
    339                 var excerptElement = $('.entry-content, .post-content, .excerpt').first();
    340                    
    341                 // Create player with specific classes
    342                 var $player = $(playerHTML)
    343                     .addClass('mementor-tts-after-title-before-excerpt')
    344                     .addClass('mementor-tts-adjacent-to-' + selector.replace(/[^a-zA-Z0-9]/g, '-'));
    345                
    346                 if (excerptElement.length) {
    347                     // Add class for excerpt context
    348                     $player.addClass('mementor-tts-before-' + excerptElement.attr('class').split(' ')[0]);
    349                     excerptElement.before($player);
    350                 } else {
    351                     // If no excerpt found, insert after title
    352                     $player.addClass('mementor-tts-no-excerpt');
    353                     titleElement.after($player);
    354                 }
    355                
    356                 initializeInsertedPlayer($player);
    357                 inserted = true;
    358                 safeDebug('Player inserted after title before excerpt using selector:', selector);
    359                 return false; // Break the loop
    360             }
    361         });
    362         break;
    363            
    364         case 'after_title_after_excerpt':
    365             var titleSelectors = [
    366                 '.entry-header',
    367                 'h1.entry-title',
    368                 '.entry-header h1',
    369                 '.post-title',
    370                 'article h1:first-of-type',
    371                 '.elementor-widget-theme-post-title h1',
    372                 '.page-title',
    373                 '.site-content h1:first-of-type',
    374                 '.wp-block-post-title',
    375                 'h1.header-title' // Added for Uncode theme support
    376             ];
    377            
    378             // Try each selector
    379             $.each(titleSelectors, function(index, selector) {
    380                 var titleElement = $(selector).first();
    381                 if (titleElement.length) {
    382                     // Find the excerpt element
    383                     var excerptElement = $('.entry-excerpt, .excerpt, .post-excerpt').first();
    384                    
    385                     // Create player with specific classes
    386                     var $player = $(playerHTML)
    387                         .addClass('mementor-tts-after-title-after-excerpt')
    388                         .addClass('mementor-tts-adjacent-to-' + selector.replace(/[^a-zA-Z0-9]/g, '-'));
    389                    
    390                     if (excerptElement.length) {
    391                         // Add class for excerpt context
    392                         $player.addClass('mementor-tts-after-' + excerptElement.attr('class').split(' ')[0]);
    393                         excerptElement.after($player);
    394                     } else {
    395                         // If no excerpt found, try to find the content
    396                         var contentElement = $('.entry-content, .post-content, article .content').first();
    397                         if (contentElement.length) {
    398                             $player.addClass('mementor-tts-before-' + contentElement.attr('class').split(' ')[0]);
    399                             contentElement.before($player);
    400                         } else {
    401                             // If no content element found either
    402                             $player.addClass('mementor-tts-no-excerpt-no-content');
    403                             titleElement.after($player);
    404                         }
    405                     }
    406                    
    407                     initializeInsertedPlayer($player);
    408                     inserted = true;
    409                     safeDebug('Player inserted after title and excerpt using selector:', selector);
    410                     return false; // Break the loop
    411                 }
    412             });
    413             break;
    414            
    415         case 'after_content':
    416             var contentSelectors = [
    417                 '.entry-content',
    418                 '.post-content',
    419                 'article .content',
    420                 '.site-content',
    421                 '.elementor-widget-theme-post-content',
    422                 '.post-entry',
    423                 '.content-area',
    424                 'article'
    425             ];
    426            
    427             // Try each selector
    428             $.each(contentSelectors, function(index, selector) {
    429                 var contentElement = $(selector).first();
    430                 if (contentElement.length) {
    431                     var $player = $(playerHTML)
    432                         .addClass('mementor-tts-after-content')
    433                         .addClass('mementor-tts-adjacent-to-' + selector.replace(/[^a-zA-Z0-9]/g, '-'));
    434                     contentElement.append($player);
    435                     initializeInsertedPlayer($player);
    436                     inserted = true;
    437                     safeDebug('Player inserted after content using selector:', selector);
    438                     return false; // Break the loop
    439                 }
    440             });
    441             break;
    442     }
    443    
    444     if (!inserted) {
    445         safeDebug('Player could not find appropriate insertion point');
    446     }
    447 
    448     // Add max-width CSS variable to the player container element after insertion
    449     function applyMaxWidthStyles() {
    450         // Return early if max width data is not defined
    451         if (typeof mementorTTSPlayerWidth === 'undefined') {
    452             safeDebug('Max width data not available');
    453             return;
    454         }
    455        
    456         // Find all player containers
    457         const playerContainers = document.querySelectorAll('.mementor-tts-player-container.mementor-tts-max-width-custom');
    458        
    459         // Get the max-width and unit from localized data
    460         const maxWidth = mementorTTSPlayerWidth.maxWidth || 100;
    461         const maxWidthUnit = mementorTTSPlayerWidth.maxWidthUnit || '%';
    462        
    463         // Apply the max-width as data attributes on each container
    464         playerContainers.forEach(container => {
    465             // Set the data attributes if not already set
    466             if (!container.hasAttribute('data-max-width')) {
    467                 container.setAttribute('data-max-width', maxWidth);
    468             }
    469             if (!container.hasAttribute('data-max-width-unit')) {
    470                 container.setAttribute('data-max-width-unit', maxWidthUnit);
    471             }
    472            
    473             // Remove any inline styles - we'll use CSS custom properties in the stylesheet
    474             if (container.hasAttribute('style') &&
    475                 container.getAttribute('style').includes('--mementor-tts-max-width')) {
    476                 container.removeAttribute('style');
    477             }
    478         });
    479     }
    480 
    481     // Call the function after the player is inserted
    482     document.addEventListener('DOMContentLoaded', function() {
    483         // Set a small delay to ensure the player is inserted
    484         setTimeout(applyMaxWidthStyles, 100);
    485     });
    486 
    487     // Also set up a mutation observer to catch dynamically inserted players
    488     const observer = new MutationObserver(function(mutations) {
    489         mutations.forEach(function(mutation) {
    490             if (mutation.addedNodes.length) {
    491                 applyMaxWidthStyles();
    492             }
    493         });
    494     });
    495 
    496     // Start observing the document body for player insertion
    497     observer.observe(document.body, { childList: true, subtree: true });
    498    
    499     // Add a final initialization function to cleanup any duplicated classes and ensure proper data attributes
    500     document.addEventListener('DOMContentLoaded', function() {
    501         setTimeout(function() {
    502             // Find all player containers
    503             const playerContainers = document.querySelectorAll('.mementor-tts-player-container');
    504            
    505             // Process each container to clean up duplicated classes
    506             playerContainers.forEach(container => {
    507                 // Remove any duplicate classes
    508                 const classNames = [];
    509                 const uniqueClasses = [];
    510                
    511                 container.classList.forEach(className => {
    512                     if (!classNames.includes(className)) {
    513                         classNames.push(className);
    514                         uniqueClasses.push(className);
    515                     }
    516                 });
    517                
    518                 // Apply the unique classes
    519                 if (classNames.length !== uniqueClasses.length) {
    520                     container.setAttribute('class', uniqueClasses.join(' '));
    521                 }
    522                
    523                 // Remove any inline styles - we'll handle through CSS only
    524                 if (container.hasAttribute('style')) {
    525                     container.removeAttribute('style');
    526                 }
    527                
    528                 // Clean up label styling if needed - convert to data attribute
    529                 const label = container.querySelector('.mementor-tts-player-label.label-color-custom');
    530                 if (label && label.hasAttribute('style')) {
    531                     const style = label.getAttribute('style');
    532                     const colorMatch = style.match(/--mementor-tts-custom-label-color:\s*([^;]+)/);
    533                     if (colorMatch && colorMatch[1]) {
    534                         // Move from style to data attribute
    535                         label.setAttribute('data-custom-color', colorMatch[1].trim());
    536                         label.removeAttribute('style');
    537                     }
    538                 }
    539             });
    540            
    541             safeDebug('Player classes and styles cleaned up');
    542         }, 500); // Delay to ensure everything is loaded
    543     });
    54440        }); // End jQuery(document).ready
    54541    } // End initializePlayerInsert
  • text-to-speech-tts/trunk/readme.txt

    r3495753 r3495969  
    66Tested up to: 6.9
    77Requires PHP: 7.2
    8 Stable tag: 3.1.3
     8Stable tag: 3.1.4
    99License: GPLv3 or later
    1010License URI: [https://www.gnu.org/licenses/gpl-3.0.txt](https://www.gnu.org/licenses/gpl-3.0.txt)
     
    221221== Screenshots ==
    2222221. Dashboard with an overview of your text-to-speech settings, ready to generate audio with one click
    223 2. Audio player added to posts automatically with no manual placement needed
    2242233. Voice and language selector. Choose from natural male and female voices in multiple languages
    225 4. Mobile-ready player with responsive design that works on any device and screen size
     2243. Audio player added to posts automatically with no manual placement needed
     2254. Word Replacement & Pronunciation Controls
    2262265. Engagement analytics. Track how many visitors listen to your audio content
    2272276. Shortcode and block generator. Place the audio player anywhere on your site
     
    236236
    237237== Changelog ==
     238
     239= 3.1.4 - 2026-03-31 =
     240
     241* Removed: Remote telemetry collection and usage tracking to external servers
     242* Fixed: License page no longer blocked for free plan users
     243* Fixed: Some posts causing a server error when auto-generation is enabled
     244* Fixed: Browser console warning about unreachable code in player script
    238245
    239246= 3.1.3 - 2026-03-31 =
  • text-to-speech-tts/trunk/text-to-speech-tts.php

    r3495753 r3495969  
    99 * Plugin URI:        https://mementor.no/en/wordpress-plugins/text-to-speech/
    1010 * Description:       The easiest Text-to-Speech plugin for WordPress. Add natural voices, boost accessibility, and engage visitors with an instant audio player.
    11  * Version:           3.1.3
     11 * Version:           3.1.4
    1212 * Author:            Mementor AS
    1313 * Author URI:        https://mementor.no/en/
     
    2626
    2727// Define plugin constants
    28 define('MEMENTOR_TTS_VERSION', '3.1.3');
     28define('MEMENTOR_TTS_VERSION', '3.1.4');
    2929define('MEMENTOR_TTS_PLUGIN_DIR', plugin_dir_path(__FILE__));
    3030define('MEMENTOR_TTS_PLUGIN_URL', plugin_dir_url(__FILE__));
     
    150150    }
    151151
     152    // Clean up removed telemetry system (added in 3.1.3)
     153    wp_clear_scheduled_hook('mementor_tts_send_remote_telemetry');
     154    delete_transient('mementor_tts_daily_usage');
     155    delete_option('mementor_tts_telemetry_consent');
     156    delete_option('mementor_tts_telemetry_consent_time');
     157
    152158    // Flush rewrite rules
    153159    flush_rewrite_rules();
    154 
    155     // Send Reddit conversion event on plugin activation
    156     mementor_tts_send_reddit_conversion();
    157 }
    158 
    159 /**
    160  * Send Reddit conversion event on plugin activation
    161  */
    162 function mementor_tts_send_reddit_conversion() {
    163     $site_url = get_site_url();
    164     $admin_email = get_option('admin_email');
    165     $wp_version = get_bloginfo('version');
    166     $plugin_version = defined('MEMENTOR_TTS_VERSION') ? MEMENTOR_TTS_VERSION : '1.0.0';
    167    
    168     // Generate a unique conversion ID
    169     $conversion_id = 'wp_tts_' . wp_generate_password(12, false);
    170    
    171     // Prepare the data to send
    172     $data = array(
    173         'event_type' => 'Installed',
    174         'conversion_id' => $conversion_id,
    175         'test_mode' => false,
    176         'event_at' => gmdate('c'),
    177         'metadata' => array(
    178             'site_url' => $site_url,
    179             'admin_email' => $admin_email,
    180             'wp_version' => $wp_version,
    181             'plugin_version' => $plugin_version,
    182             'plugin_name' => 'text-to-speech-tts'
    183         )
    184     );
    185    
    186     // Send the POST request
    187     $response = wp_remote_post('https://mementor.no/api/reddit-conversion/', array(
    188         'method' => 'POST',
    189         'timeout' => 30,
    190         'headers' => array(
    191             'Content-Type' => 'application/json'
    192         ),
    193         'body' => wp_json_encode($data)
    194     ));
    195    
    196     // Log the response for debugging (optional)
    197     if (is_wp_error($response)) {
    198         error_log('Reddit conversion error: ' . $response->get_error_message());
    199     } else {
    200         $response_code = wp_remote_retrieve_response_code($response);
    201         $response_body = wp_remote_retrieve_body($response);
    202        
    203         if ($response_code !== 200) {
    204             error_log('Reddit conversion failed with code ' . $response_code . ': ' . $response_body);
    205         } else {
    206             // Store the conversion ID for potential future use
    207             update_option('mementor_tts_reddit_conversion_id', $conversion_id, false);
    208         }
    209     }
    210160}
    211161
     
    219169    wp_clear_scheduled_hook('mementor_tts_aggregate_player_stats');
    220170    wp_clear_scheduled_hook('mementor_tts_aggregate_analytics');
     171    wp_clear_scheduled_hook('mementor_tts_send_remote_telemetry');
     172
     173    // Clean up telemetry data
     174    delete_transient('mementor_tts_daily_usage');
     175    delete_option('mementor_tts_telemetry_consent');
     176    delete_option('mementor_tts_telemetry_consent_time');
    221177
    222178    // Flush rewrite rules
Note: See TracChangeset for help on using the changeset viewer.