Changeset 3494985
- Timestamp:
- 03/30/2026 10:36:36 PM (5 days ago)
- Location:
- text-to-speech-tts/trunk
- Files:
-
- 10 edited
-
admin/js/mementor-tts-admin-columns.js (modified) (1 diff)
-
admin/js/mementor-tts-audio-generation.js (modified) (1 diff)
-
admin/js/mementor-tts-modal-utils.js (modified) (1 diff)
-
admin/partials/pages/settings.php (modified) (1 diff)
-
includes/class-mementor-tts-ajax.php (modified) (2 diffs)
-
includes/class-mementor-tts-connect.php (modified) (1 diff)
-
includes/class-mementor-tts-processor.php (modified) (1 diff)
-
includes/class-mementor-tts.php (modified) (1 diff)
-
readme.txt (modified) (2 diffs)
-
text-to-speech-tts.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
text-to-speech-tts/trunk/admin/js/mementor-tts-admin-columns.js
r3493900 r3494985 80 80 var errorMessage = response.data ? (response.data.message || response.data) : (window.mementorTTSAudio.errorGeneric || 'Error generating audio'); 81 81 82 // Check if site is not connected 83 if (response.data && response.data.error === 'not_connected') { 84 if (typeof window.mementorTTSModalUtils !== 'undefined' && 85 typeof window.mementorTTSModalUtils.showNotConnected === 'function') { 86 window.mementorTTSModalUtils.showNotConnected(); 87 } else { 88 window.mementorTTSShowNotification( 89 'Site is not connected. Go to Settings to connect your site.', 90 'error', 91 0 92 ); 93 } 94 button.html(originalHtml).removeClass('mementor-tts-disabled'); 95 delete window.processingPosts[postId]; 96 return; 97 } 98 82 99 // Check for voice_limit_reached in all possible locations 83 100 const responseStr = JSON.stringify(response); -
text-to-speech-tts/trunk/admin/js/mementor-tts-audio-generation.js
r3493900 r3494985 101 101 if (typeof response.data !== 'undefined' && typeof response.data.message !== 'undefined') { 102 102 if (typeof window.mementorTTSShowNotification === 'function') { 103 // Check if site is not connected 104 if (response.data.error === 'not_connected') { 105 if (typeof window.mementorTTSModalUtils !== 'undefined' && 106 typeof window.mementorTTSModalUtils.showNotConnected === 'function') { 107 window.mementorTTSModalUtils.showNotConnected(); 108 } else { 109 window.mementorTTSShowNotification( 110 'Site is not connected. Go to Settings to connect your site.', 111 'error', 112 0 113 ); 114 } 115 button.html(originalHtml); 116 return; 117 } 118 103 119 // Check if this is a quota exceeded error 104 120 const errorMessage = response.data.message; -
text-to-speech-tts/trunk/admin/js/mementor-tts-modal-utils.js
r3493900 r3494985 430 430 431 431 /** 432 * Shows a modern "site not connected" dialog directing users to the Settings page. 433 * Self-contained with inline styles matching the TTSWP design language. 434 */ 435 window.mementorTTSModalUtils.showNotConnected = function() { 436 // Inject keyframes once 437 if (!document.getElementById('tts-nc-keyframes')) { 438 var style = document.createElement('style'); 439 style.id = 'tts-nc-keyframes'; 440 style.textContent = 441 '@keyframes ttsNcFadeIn{from{opacity:0}to{opacity:1}}' + 442 '@keyframes ttsNcSlideIn{from{opacity:0;transform:scale(.95) translateY(-10px)}to{opacity:1;transform:scale(1) translateY(0)}}'; 443 document.head.appendChild(style); 444 } 445 446 // Overlay 447 var modal = document.createElement('div'); 448 modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(15,23,42,.7);backdrop-filter:blur(4px);z-index:999999;display:flex;align-items:center;justify-content:center;padding:20px;animation:ttsNcFadeIn .3s ease-out'; 449 450 // Dialog 451 var dialog = document.createElement('div'); 452 dialog.style.cssText = 'background:linear-gradient(180deg,#fff 0%,#f8fafc 100%);border-radius:20px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25),0 0 0 1px rgba(0,0,0,.05);max-width:420px;width:100%;overflow:hidden;animation:ttsNcSlideIn .3s ease-out'; 453 454 // Logo wrap 455 var logoWrap = document.createElement('div'); 456 logoWrap.style.cssText = 'display:flex;justify-content:center;padding:32px 24px 0'; 457 var logoBox = document.createElement('div'); 458 logoBox.style.cssText = 'width:64px;height:64px;background:#18181b;border-radius:16px;display:flex;align-items:center;justify-content:center;box-shadow:0 10px 20px -5px rgba(24,24,27,.4)'; 459 var logoImg = document.createElement('img'); 460 logoImg.src = (window.mementorTTSAudio && window.mementorTTSAudio.pluginUrl ? window.mementorTTSAudio.pluginUrl : '') + 'admin/images/ttswp-icon.svg'; 461 logoImg.alt = 'TTSWP'; 462 logoImg.style.cssText = 'width:48px;height:48px'; 463 logoBox.appendChild(logoImg); 464 logoWrap.appendChild(logoBox); 465 466 // Body 467 var body = document.createElement('div'); 468 body.style.cssText = 'padding:24px 28px 20px;text-align:center'; 469 470 var h2 = document.createElement('h2'); 471 h2.textContent = 'Site Not Connected'; 472 h2.style.cssText = 'margin:0 0 8px;font-size:22px;font-weight:700;color:#1e293b'; 473 body.appendChild(h2); 474 475 var subtitle = document.createElement('p'); 476 subtitle.textContent = 'Connect your site to start generating audio with natural AI voices.'; 477 subtitle.style.cssText = 'margin:0 0 20px;color:#64748b;font-size:14px;line-height:1.5'; 478 body.appendChild(subtitle); 479 480 // Feature pills 481 var pills = document.createElement('div'); 482 pills.style.cssText = 'display:flex;justify-content:center;gap:8px;flex-wrap:wrap;margin-bottom:8px'; 483 484 var pillData = [ 485 {icon: '\u{1F3A4}', text: '10,000 Free Credits'}, 486 {icon: '\u{26A1}', text: 'Instant Setup'}, 487 {icon: '\u{1F512}', text: 'Secure Connection'} 488 ]; 489 490 pillData.forEach(function(item) { 491 var pill = document.createElement('span'); 492 pill.style.cssText = 'display:inline-flex;align-items:center;gap:4px;background:#f0ebfd;color:#4a2fa0;font-size:12px;font-weight:600;padding:6px 12px;border-radius:20px;border:1px solid #c9b8f5'; 493 pill.textContent = item.icon + ' ' + item.text; 494 pills.appendChild(pill); 495 }); 496 body.appendChild(pills); 497 498 // Footer with buttons 499 var footer = document.createElement('div'); 500 footer.style.cssText = 'padding:0 28px 24px'; 501 502 var connectBtn = document.createElement('button'); 503 connectBtn.textContent = 'Connect Your Site'; 504 connectBtn.style.cssText = 'width:100%;padding:14px 20px;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;border:none;color:#fff;background:linear-gradient(135deg,#6B45D4 0%,#5435B0 100%);box-shadow:0 4px 12px -2px rgba(107,69,212,.4);transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:8px'; 505 connectBtn.onmouseenter = function() { this.style.transform = 'translateY(-1px)'; this.style.boxShadow = '0 6px 16px -2px rgba(107,69,212,.5)'; }; 506 connectBtn.onmouseleave = function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 4px 12px -2px rgba(107,69,212,.4)'; }; 507 508 var dismissBtn = document.createElement('button'); 509 dismissBtn.textContent = 'Not now'; 510 dismissBtn.style.cssText = 'width:100%;padding:10px;margin-top:8px;border:none;background:transparent;color:#94a3b8;font-size:13px;cursor:pointer;transition:color .2s'; 511 dismissBtn.onmouseenter = function() { this.style.color = '#64748b'; }; 512 dismissBtn.onmouseleave = function() { this.style.color = '#94a3b8'; }; 513 514 // Events 515 connectBtn.addEventListener('click', function() { 516 document.body.removeChild(modal); 517 window.location.href = 'admin.php?page=text-to-speech-tts-settings'; 518 }); 519 dismissBtn.addEventListener('click', function() { 520 document.body.removeChild(modal); 521 }); 522 modal.addEventListener('click', function(e) { 523 if (e.target === modal) document.body.removeChild(modal); 524 }); 525 526 footer.appendChild(connectBtn); 527 footer.appendChild(dismissBtn); 528 529 // Assemble 530 dialog.appendChild(logoWrap); 531 dialog.appendChild(body); 532 dialog.appendChild(footer); 533 modal.appendChild(dialog); 534 document.body.appendChild(modal); 535 536 return modal; 537 }; 538 539 /** 432 540 * Shows an API key error dialog 433 * 541 * 434 542 * @param {boolean} hasNoApiKey - Whether the user has no API key configured 435 543 * @param {string} errorMessage - The error message from the API -
text-to-speech-tts/trunk/admin/partials/pages/settings.php
r3493990 r3494985 1533 1533 </script> 1534 1534 1535 <?php1536 // ── Telemetry consent modal ──1537 $telemetry_consent = get_option('mementor_tts_telemetry_consent', '');1538 $telemetry_consent_time = get_option('mementor_tts_telemetry_consent_time', 0);1539 $days_since_ask = $telemetry_consent_time ? floor((time() - $telemetry_consent_time) / DAY_IN_SECONDS) : 999;1540 $show_modal = ($telemetry_consent === '') || ($telemetry_consent === 'no' && $days_since_ask >= 30);1541 1542 if ($show_modal) :1543 ?>1544 <div id="mementor-tts-telemetry-modal" class="mementor-tts-telemetry-overlay">1545 <div class="mementor-tts-telemetry-dialog">1546 <div class="mementor-tts-telemetry-icon-wrap">1547 <div class="mementor-tts-telemetry-icon">1548 <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>1549 </div>1550 </div>1551 <div class="mementor-tts-telemetry-body">1552 <h2><?php esc_html_e('Help Us Improve', 'text-to-speech-tts'); ?></h2>1553 <p class="mementor-tts-telemetry-subtitle"><?php esc_html_e('Share anonymous usage statistics to help make Text to Speech better for everyone.', 'text-to-speech-tts'); ?></p>1554 <div class="mementor-tts-telemetry-columns">1555 <div class="mementor-tts-telemetry-col mementor-tts-telemetry-yes-col">1556 <div class="mementor-tts-telemetry-col-header">1557 <span class="mementor-tts-check-icon">✓</span>1558 <?php esc_html_e('What we collect', 'text-to-speech-tts'); ?>1559 </div>1560 <ul>1561 <li><?php esc_html_e('Usage counts', 'text-to-speech-tts'); ?></li>1562 <li><?php esc_html_e('Plugin version', 'text-to-speech-tts'); ?></li>1563 <li><?php esc_html_e('Site domain', 'text-to-speech-tts'); ?></li>1564 </ul>1565 </div>1566 <div class="mementor-tts-telemetry-col mementor-tts-telemetry-no-col">1567 <div class="mementor-tts-telemetry-col-header">1568 <span class="mementor-tts-x-icon">✕</span>1569 <?php esc_html_e('Never collected', 'text-to-speech-tts'); ?>1570 </div>1571 <ul>1572 <li><?php esc_html_e('Personal data', 'text-to-speech-tts'); ?></li>1573 <li><?php esc_html_e('Your content', 'text-to-speech-tts'); ?></li>1574 <li><?php esc_html_e('API keys', 'text-to-speech-tts'); ?></li>1575 </ul>1576 </div>1577 </div>1578 </div>1579 <div class="mementor-tts-telemetry-footer">1580 <button type="button" id="mementor-tts-telemetry-no" class="mementor-tts-telemetry-btn mementor-tts-telemetry-btn-secondary">1581 <?php esc_html_e('No', 'text-to-speech-tts'); ?>1582 </button>1583 <button type="button" id="mementor-tts-telemetry-yes" class="mementor-tts-telemetry-btn mementor-tts-telemetry-btn-primary">1584 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>1585 <?php esc_html_e('Sure, why not', 'text-to-speech-tts'); ?>1586 </button>1587 </div>1588 <p class="mementor-tts-telemetry-note"><?php esc_html_e('You can change this anytime in Settings → Advanced', 'text-to-speech-tts'); ?></p>1589 </div>1590 </div>1591 1592 <style>1593 .mementor-tts-telemetry-overlay {1594 position: fixed !important;1595 top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;1596 background: rgba(15, 23, 42, 0.7) !important;1597 backdrop-filter: blur(4px);1598 z-index: 999999 !important;1599 display: none;1600 align-items: center;1601 justify-content: center;1602 padding: 20px;1603 }1604 .mementor-tts-telemetry-overlay.active { display: flex !important; animation: ttsModalFadeIn 0.3s ease-out; }1605 @keyframes ttsModalFadeIn { from { opacity: 0; } to { opacity: 1; } }1606 @keyframes ttsModalSlideIn { from { opacity: 0; transform: scale(0.95) translateY(-10px); } to { opacity: 1; transform: scale(1) translateY(0); } }1607 .mementor-tts-telemetry-dialog {1608 background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);1609 border-radius: 20px;1610 box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.05);1611 max-width: 440px; width: 100%;1612 overflow: hidden;1613 animation: ttsModalSlideIn 0.3s ease-out;1614 }1615 .mementor-tts-telemetry-icon-wrap { display: flex; justify-content: center; padding: 32px 24px 0; }1616 .mementor-tts-telemetry-icon {1617 width: 64px; height: 64px;1618 background: linear-gradient(135deg, var(--tts-purple) 0%, var(--tts-purple-dark) 100%);1619 border-radius: 16px;1620 display: flex; align-items: center; justify-content: center;1621 color: white;1622 box-shadow: 0 10px 20px -5px rgba(107, 69, 212, 0.4);1623 }1624 .mementor-tts-telemetry-body { padding: 24px 28px 20px; text-align: center; }1625 .mementor-tts-telemetry-body h2 { margin: 0 0 8px; font-size: 22px; font-weight: 700; color: #1e293b; }1626 .mementor-tts-telemetry-subtitle { margin: 0 0 24px !important; color: #64748b !important; font-size: 14px !important; line-height: 1.5 !important; }1627 .mementor-tts-telemetry-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 8px; }1628 .mementor-tts-telemetry-col { background: #fff; border-radius: 12px; padding: 16px; text-align: left; border: 1px solid #e2e8f0; }1629 .mementor-tts-telemetry-col-header { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 12px; color: #475569; }1630 .mementor-tts-check-icon { width: 20px; height: 20px; background: #dcfce7; color: #16a34a; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: bold; }1631 .mementor-tts-x-icon { width: 20px; height: 20px; background: #fee2e2; color: #dc2626; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: bold; }1632 .mementor-tts-telemetry-col ul { margin: 0; padding: 0; list-style: none; }1633 .mementor-tts-telemetry-col li { font-size: 14px; color: #64748b; padding: 4px 0; display: flex; align-items: center; gap: 6px; }1634 .mementor-tts-telemetry-col li::before { content: ''; width: 4px; height: 4px; background: #cbd5e1; border-radius: 50%; flex-shrink: 0; }1635 .mementor-tts-telemetry-footer { display: flex; gap: 12px; padding: 0 28px 24px; }1636 .mementor-tts-telemetry-btn { flex: 1; padding: 12px 20px; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; border: none; }1637 .mementor-tts-telemetry-btn-secondary { background: #f1f5f9; color: #475569; }1638 .mementor-tts-telemetry-btn-secondary:hover { background: #e2e8f0; color: #334155; }1639 .mementor-tts-telemetry-btn-primary { background: linear-gradient(135deg, var(--tts-purple) 0%, var(--tts-purple-dark) 100%); color: white; box-shadow: 0 4px 12px -2px rgba(107, 69, 212, 0.4); }1640 .mementor-tts-telemetry-btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 16px -2px rgba(107, 69, 212, 0.5); }1641 .mementor-tts-telemetry-note { text-align: center; font-size: 11px; color: #94a3b8; padding: 0 28px 20px; margin: 0; }1642 </style>1643 1644 <script>1645 jQuery(document).ready(function($) {1646 var $modal = $('#mementor-tts-telemetry-modal');1647 setTimeout(function() { $modal.addClass('active'); }, 1000);1648 1649 function handleConsent(consent) {1650 $.ajax({1651 url: ajaxurl,1652 type: 'POST',1653 data: {1654 action: 'mementor_tts_telemetry_consent',1655 nonce: '<?php echo esc_js(wp_create_nonce('mementor_tts_nonce')); ?>',1656 consent: consent1657 },1658 complete: function() { $modal.removeClass('active'); }1659 });1660 }1661 1662 $('#mementor-tts-telemetry-yes').on('click', function() { handleConsent('yes'); });1663 $('#mementor-tts-telemetry-no').on('click', function() { handleConsent('no'); });1664 $modal.on('click', function(e) { if (e.target === this) $modal.removeClass('active'); });1665 $(document).on('keydown', function(e) { if (e.key === 'Escape' && $modal.hasClass('active')) $modal.removeClass('active'); });1666 });1667 </script>1668 <?php endif; ?>1669 1535 1670 1536 <?php endif; /* end Mementor_TTS_Connect connected check */ ?> -
text-to-speech-tts/trunk/includes/class-mementor-tts-ajax.php
r3493900 r3494985 51 51 add_action('wp_ajax_nopriv_mementor_tts_generate_shortcode_audio', array($this, 'ajax_generate_shortcode_audio')); 52 52 53 // Telemetry consent54 add_action('wp_ajax_mementor_tts_telemetry_consent', array($this, 'handle_telemetry_consent'));55 56 53 // Review dismiss 57 54 add_action('wp_ajax_mementor_tts_dismiss_review', array($this, 'handle_dismiss_review')); … … 461 458 'filename' => $zip_filename, 462 459 'count' => $added, 463 ));464 }465 466 /**467 * Handle telemetry consent AJAX request468 */469 public function handle_telemetry_consent() {470 // Check nonce471 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {472 wp_send_json_error(array('message' => __('Invalid security token.', 'text-to-speech-tts')));473 return;474 }475 476 // Check user capabilities477 if (!current_user_can('manage_options')) {478 wp_send_json_error(array('message' => __('Permission denied.', 'text-to-speech-tts')));479 return;480 }481 482 $consent = isset($_POST['consent']) ? sanitize_text_field(wp_unslash($_POST['consent'])) : '';483 484 if (!in_array($consent, array('yes', 'no'), true)) {485 wp_send_json_error(array('message' => __('Invalid consent value.', 'text-to-speech-tts')));486 return;487 }488 489 // Save consent490 update_option('mementor_tts_telemetry_consent', $consent, false);491 update_option('mementor_tts_telemetry_consent_time', time(), false);492 493 wp_send_json_success(array(494 'message' => $consent === 'yes'495 ? __('Thank you for helping improve the plugin!', 'text-to-speech-tts')496 : __('No problem. You can change this anytime in Advanced settings.', 'text-to-speech-tts')497 460 )); 498 461 } -
text-to-speech-tts/trunk/includes/class-mementor-tts-connect.php
r3493900 r3494985 76 76 $api_key = get_option( 'mementor_tts_api_key', '' ); 77 77 if ( ! empty( $api_key ) ) { 78 if ( ! class_exists( 'Mementor_TTS_Encryption' ) ) { 79 require_once plugin_dir_path( __FILE__ ) . 'class-mementor-tts-encryption.php'; 80 } 78 81 if ( class_exists( 'Mementor_TTS_Encryption' ) ) { 79 $decrypted = Mementor_TTS_Encryption::decrypt( $api_key ); 82 $enc = new Mementor_TTS_Encryption(); 83 $decrypted = $enc->decrypt( $api_key ); 80 84 if ( $decrypted ) { 81 85 $api_key = $decrypted; -
text-to-speech-tts/trunk/includes/class-mementor-tts-processor.php
r3493900 r3494985 1052 1052 // Release lock before returning 1053 1053 delete_transient($lock_key); 1054 1055 // Check for not connected 1056 if ($response->get_error_code() === 'not_connected') { 1057 return array( 1058 'success' => false, 1059 'error' => 'not_connected', 1060 'message' => $error_message, 1061 ); 1062 } 1054 1063 1055 1064 // Check for credit exhaustion -
text-to-speech-tts/trunk/includes/class-mementor-tts.php
r3493900 r3494985 496 496 */ 497 497 public function run() { 498 // Initialize telemetry (singleton will handle it)499 if (!class_exists('Mementor_TTS_Remote_Telemetry')) {500 require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-remote-telemetry.php';501 }502 Mementor_TTS_Remote_Telemetry::get_instance();503 504 498 $this->loader->run(); 505 499 } -
text-to-speech-tts/trunk/readme.txt
r3493990 r3494985 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.2 8 Stable tag: 3.1. 18 Stable tag: 3.1.2 9 9 License: GPLv3 or later 10 10 License URI: [https://www.gnu.org/licenses/gpl-3.0.txt](https://www.gnu.org/licenses/gpl-3.0.txt) … … 236 236 237 237 == Changelog == 238 239 = 3.1.2 - 2026-03-30 = 240 241 * Improved: "Site not connected" error now shows a helpful modal with a link to Settings instead of a plain message 242 * Removed: Telemetry consent modal and remote telemetry collection 238 243 239 244 = 3.1.1 - 2026-03-29 = -
text-to-speech-tts/trunk/text-to-speech-tts.php
r3493990 r3494985 9 9 * Plugin URI: https://mementor.no/en/wordpress-plugins/text-to-speech/ 10 10 * 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. 111 * Version: 3.1.2 12 12 * Author: Mementor AS 13 13 * Author URI: https://mementor.no/en/ … … 26 26 27 27 // Define plugin constants 28 define('MEMENTOR_TTS_VERSION', '3.1. 1');28 define('MEMENTOR_TTS_VERSION', '3.1.2'); 29 29 define('MEMENTOR_TTS_PLUGIN_DIR', plugin_dir_path(__FILE__)); 30 30 define('MEMENTOR_TTS_PLUGIN_URL', plugin_dir_url(__FILE__)); … … 218 218 wp_clear_scheduled_hook('mementor_tts_cleanup_audio'); 219 219 wp_clear_scheduled_hook('mementor_tts_aggregate_player_stats'); 220 wp_clear_scheduled_hook('mementor_tts_send_telemetry');221 wp_clear_scheduled_hook('mementor_tts_send_remote_telemetry');222 220 wp_clear_scheduled_hook('mementor_tts_aggregate_analytics'); 223 221
Note: See TracChangeset
for help on using the changeset viewer.