diff --git a/native/wordpress/maple-fonts-wp/assets/admin.css b/native/wordpress/maple-fonts-wp/assets/admin.css index 6858277..1a923f3 100644 --- a/native/wordpress/maple-fonts-wp/assets/admin.css +++ b/native/wordpress/maple-fonts-wp/assets/admin.css @@ -71,33 +71,26 @@ /* Search Input */ .mlf-search-wrapper { - position: relative; + display: flex; + align-items: center; + gap: 8px; max-width: 500px; } -.mlf-search-icon { - position: absolute; - left: 10px; - top: 50%; - transform: translateY(-50%); - color: #646970; - font-size: 18px; - width: 18px; - height: 18px; +.mlf-search-input { + flex: 1; + padding: 6px 12px !important; + font-size: 14px; } -.mlf-search-input { - width: 100% !important; - max-width: none !important; - padding-left: 36px !important; - padding-right: 36px !important; +.mlf-search-btn { + flex-shrink: 0; + height: 36px; + padding: 0 16px !important; } .mlf-search-spinner { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); + flex-shrink: 0; float: none !important; margin: 0 !important; } @@ -183,6 +176,14 @@ font-style: italic; } +.mlf-search-error { + padding: 16px 24px; + text-align: center; + color: #8b6914; + background: #fcf9e8; + border-bottom: 1px solid #f0e6c8; +} + /* Selected Font */ .mlf-selected-font { margin-top: 16px; @@ -313,6 +314,26 @@ margin-top: 1px; } +/* Section Header */ +.mlf-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid #e0e0e0; +} + +.mlf-section-header h2 { + margin: 0; + padding: 0; + border: none; +} + +.mlf-check-updates-btn { + flex-shrink: 0; +} + /* Font List */ .mlf-font-list { display: flex; @@ -334,17 +355,61 @@ flex: 1; } +.mlf-font-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 4px; +} + .mlf-font-name { - margin: 0 0 4px 0; + margin: 0; font-size: 1.1em; } +.mlf-update-badge { + display: inline-block; + padding: 2px 8px; + background: #fcf9e8; + border: 1px solid #d4b106; + border-radius: 3px; + color: #8b6914; + font-size: 11px; + font-weight: 500; +} + .mlf-font-variants { - margin: 0; + margin: 0 0 4px 0; color: #646970; font-size: 0.9em; } +.mlf-font-meta { + margin: 0; + color: #8c8f94; + font-size: 0.8em; + display: flex; + gap: 12px; +} + +.mlf-font-version { + font-family: monospace; + background: #f0f0f1; + padding: 1px 6px; + border-radius: 3px; +} + +.mlf-update-btn { + color: #2271b1; + border-color: #2271b1; +} + +.mlf-update-btn:hover { + background: #2271b1; + color: #fff; + border-color: #2271b1; +} + .mlf-font-actions { margin-left: 20px; } diff --git a/native/wordpress/maple-fonts-wp/assets/admin.js b/native/wordpress/maple-fonts-wp/assets/admin.js index fff43ea..bcbfa71 100644 --- a/native/wordpress/maple-fonts-wp/assets/admin.js +++ b/native/wordpress/maple-fonts-wp/assets/admin.js @@ -18,6 +18,12 @@ */ selectedFont: null, + /** + * Currently selected font version info. + */ + selectedFontVersion: null, + selectedFontLastModified: null, + /** * Loaded font preview stylesheets. */ @@ -34,19 +40,25 @@ * Bind event handlers. */ bindEvents: function() { - // Search input - $('#mlf-font-search').on('input', this.handleSearchInput.bind(this)); + // Search button click + $('#mlf-search-btn').on('click', this.handleSearchClick.bind(this)); + + // Search on Enter key + $('#mlf-font-search').on('keypress', this.handleSearchKeypress.bind(this)); // Click outside to close search results $(document).on('click', this.handleDocumentClick.bind(this)); - // Prevent closing when clicking inside search area + // Prevent closing when clicking inside search area (but not on result items) $('.mlf-import-section').on('click', function(e) { - e.stopPropagation(); + // Don't stop propagation if clicking on a result item + if (!$(e.target).closest('.mlf-result-item').length) { + e.stopPropagation(); + } }); - // Font selection from results - $(document).on('click', '.mlf-result-item', this.handleFontSelect.bind(this)); + // Font selection from results - bind to results container + $('#mlf-search-results').on('click', '.mlf-result-item', this.handleFontSelect.bind(this)); // Change font button $('#mlf-change-font').on('click', this.handleChangeFont.bind(this)); @@ -56,31 +68,48 @@ // Delete button clicks $(document).on('click', '.mlf-delete-btn', this.handleDelete.bind(this)); + + // Check for updates button + $('#mlf-check-updates').on('click', this.handleCheckUpdates.bind(this)); + + // Update button clicks + $(document).on('click', '.mlf-update-btn', this.handleUpdateFont.bind(this)); }, /** - * Handle search input (debounced). + * Handle search button click. * - * @param {Event} e Input event. + * @param {Event} e Click event. */ - handleSearchInput: function(e) { - var query = $(e.target).val().trim(); + handleSearchClick: function(e) { + e.preventDefault(); + this.triggerSearch(); + }, - // Clear previous timer - if (this.searchTimer) { - clearTimeout(this.searchTimer); + /** + * Handle Enter key in search input. + * + * @param {Event} e Keypress event. + */ + handleSearchKeypress: function(e) { + if (e.which === 13) { + e.preventDefault(); + this.triggerSearch(); } + }, + + /** + * Trigger a search with the current input value. + */ + triggerSearch: function() { + var query = $('#mlf-font-search').val().trim(); - // Hide results if query too short if (query.length < 2) { - this.hideSearchResults(); + this.displayError(mapleLocalFontsData.strings.minChars || 'Please enter at least 2 characters.'); return; } - // Debounce: wait 300ms before searching - this.searchTimer = setTimeout(function() { - MLF.performSearch(query); - }, 300); + this.performSearch(query); }, /** @@ -90,11 +119,11 @@ */ performSearch: function(query) { var $spinner = $('#mlf-search-spinner'); - var $results = $('#mlf-search-results'); - var $list = $('#mlf-results-list'); + var $button = $('#mlf-search-btn'); - // Show spinner + // Show spinner, disable button $spinner.addClass('is-active'); + $button.prop('disabled', true); // Send AJAX request $.ajax({ @@ -106,17 +135,30 @@ query: query }, success: function(response) { - if (response.success && response.data.fonts) { - MLF.displaySearchResults(response.data.fonts); + if (response.success && response.data && response.data.fonts) { + if (response.data.fonts.length > 0) { + MLF.displaySearchResults(response.data.fonts); + } else { + MLF.displayNoResults(); + } } else { - MLF.displayNoResults(); + // Handle error response + var errorMsg = (response.data && response.data.message) + ? response.data.message + : (mapleLocalFontsData.strings.error || 'An error occurred.'); + MLF.displayError(errorMsg); } }, - error: function() { - MLF.displayNoResults(); + error: function(xhr) { + var errorMsg = mapleLocalFontsData.strings.error || 'An error occurred.'; + if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { + errorMsg = xhr.responseJSON.data.message; + } + MLF.displayError(errorMsg); }, complete: function() { $spinner.removeClass('is-active'); + $button.prop('disabled', false); } }); }, @@ -130,15 +172,21 @@ var $results = $('#mlf-search-results'); var $list = $('#mlf-results-list'); - if (fonts.length === 0) { + if (!fonts || fonts.length === 0) { this.displayNoResults(); return; } var html = ''; var previewText = mapleLocalFontsData.strings.previewText || 'Maple Fonts Preview'; + var validFonts = 0; fonts.forEach(function(font) { + // Skip fonts with missing family name + if (!font || !font.family) { + return; + } + var fontFamily = font.family; var category = font.category || 'sans-serif'; var categoryLabel = MLF.getCategoryLabel(category); @@ -148,7 +196,7 @@ badges.push('Variable'); } - html += '
'; + html += '
'; html += '
'; html += ' ' + MLF.escapeHtml(fontFamily) + ''; html += ' ' + MLF.escapeHtml(categoryLabel) + ''; @@ -163,8 +211,15 @@ // Load font for preview MLF.loadFontPreview(fontFamily); + validFonts++; }); + // If no valid fonts were found, show no results + if (validFonts === 0) { + this.displayNoResults(); + return; + } + $list.html(html); $results.show(); }, @@ -175,12 +230,25 @@ displayNoResults: function() { var $results = $('#mlf-search-results'); var $list = $('#mlf-results-list'); - var message = mapleLocalFontsData.strings.noResults || 'No fonts found.'; + var message = mapleLocalFontsData.strings.noResults || 'No fonts found. Try a different search term.'; $list.html('
' + MLF.escapeHtml(message) + '
'); $results.show(); }, + /** + * Display an error message in search results. + * + * @param {string} message Error message. + */ + displayError: function(message) { + var $results = $('#mlf-search-results'); + var $list = $('#mlf-results-list'); + + $list.html('
' + MLF.escapeHtml(message) + '
'); + $results.show(); + }, + /** * Hide search results. */ @@ -212,9 +280,9 @@ // Mark as loading this.loadedFonts[fontFamily] = true; - // Create Google Fonts link + // Create Google Fonts link (spaces become %20 which Google accepts) var fontUrl = 'https://fonts.googleapis.com/css2?family=' + - encodeURIComponent(fontFamily.replace(/ /g, '+')) + + encodeURIComponent(fontFamily) + ':wght@400&display=swap'; // Create and append link element @@ -235,8 +303,10 @@ var $item = $(e.currentTarget); var fontFamily = $item.data('font-family'); - // Store selected font + // Store selected font and version info this.selectedFont = fontFamily; + this.selectedFontVersion = $item.data('font-version') || ''; + this.selectedFontLastModified = $item.data('font-modified') || ''; // Update UI $('#mlf-font-name').val(fontFamily); @@ -351,7 +421,9 @@ action: 'mlf_download_font', nonce: mapleLocalFontsData.downloadNonce, font_name: fontName, - include_italic: includeItalic + include_italic: includeItalic, + font_version: this.selectedFontVersion || '', + font_last_modified: this.selectedFontLastModified || '' }, success: function(response) { if (response.success) { @@ -456,6 +528,135 @@ .addClass('mlf-message-' + type) .text(message) .show(); + }, + + /** + * Handle check for updates button click. + * + * @param {Event} e Click event. + */ + handleCheckUpdates: function(e) { + e.preventDefault(); + + var $button = $('#mlf-check-updates'); + var originalText = $button.text(); + + // Disable button and show checking state + $button.prop('disabled', true).text(mapleLocalFontsData.strings.checking || 'Checking...'); + + // Send AJAX request + $.ajax({ + url: mapleLocalFontsData.ajaxUrl, + type: 'POST', + data: { + action: 'mlf_check_updates', + nonce: mapleLocalFontsData.checkUpdatesNonce + }, + success: function(response) { + if (response.success) { + var updates = response.data.updates || {}; + var updateCount = Object.keys(updates).length; + + // Update UI for each font + $('.mlf-font-item').each(function() { + var $item = $(this); + var fontId = $item.data('font-id'); + var $badge = $item.find('.mlf-update-badge'); + var $updateBtn = $item.find('.mlf-update-btn'); + + if (updates[fontId]) { + // Show update available badge and button + $badge.show(); + $updateBtn.show(); + + // Store the latest version info on the button + $updateBtn.data('latest-version', updates[fontId].latest_version); + } else { + // Hide update badge and button + $badge.hide(); + $updateBtn.hide(); + } + }); + + // Show summary message + if (updateCount > 0) { + var message = mapleLocalFontsData.strings.updatesFound || 'Updates available for %d font(s).'; + alert(message.replace('%d', updateCount)); + } else { + alert(mapleLocalFontsData.strings.noUpdates || 'All fonts are up to date.'); + } + } else { + alert(response.data.message || mapleLocalFontsData.strings.error); + } + }, + error: function(xhr) { + var message = mapleLocalFontsData.strings.error; + if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { + message = xhr.responseJSON.data.message; + } + alert(message); + }, + complete: function() { + $button.prop('disabled', false).text(originalText); + } + }); + }, + + /** + * Handle font update button click. + * + * @param {Event} e Click event. + */ + handleUpdateFont: function(e) { + e.preventDefault(); + + var $button = $(e.currentTarget); + var fontId = $button.data('font-id'); + var fontName = $button.data('font-name'); + var $fontItem = $button.closest('.mlf-font-item'); + + // Confirm update + if (!confirm('Update ' + fontName + ' to the latest version?')) { + return; + } + + // Store original button text + var originalText = $button.text(); + + // Disable button and show updating state + $button.prop('disabled', true).text(mapleLocalFontsData.strings.updating || 'Updating...'); + $fontItem.addClass('mlf-loading'); + + // Send AJAX request + $.ajax({ + url: mapleLocalFontsData.ajaxUrl, + type: 'POST', + data: { + action: 'mlf_update_font', + nonce: mapleLocalFontsData.updateFontNonce, + font_id: fontId + }, + success: function(response) { + if (response.success) { + // Reload page to show updated font + alert(response.data.message); + window.location.reload(); + } else { + alert(response.data.message || mapleLocalFontsData.strings.error); + $button.prop('disabled', false).text(originalText); + $fontItem.removeClass('mlf-loading'); + } + }, + error: function(xhr) { + var message = mapleLocalFontsData.strings.error; + if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { + message = xhr.responseJSON.data.message; + } + alert(message); + $button.prop('disabled', false).text(originalText); + $fontItem.removeClass('mlf-loading'); + } + }); } }; diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php index dc33bd3..725df74 100644 --- a/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php @@ -49,15 +49,17 @@ class MLF_Admin_Page {
- +
-

+

@@ -99,22 +101,32 @@ class MLF_Admin_Page {
- +
-

+
+

+ + + +

-
+
-

+
+

+ +

+

+ + + + + + +

+ diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php index 972a86a..4a90034 100644 --- a/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php @@ -29,6 +29,8 @@ class MLF_Ajax_Handler { public function __construct() { add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']); add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']); + add_action('wp_ajax_mlf_check_updates', [$this, 'handle_check_updates']); + add_action('wp_ajax_mlf_update_font', [$this, 'handle_update_font']); // NEVER add wp_ajax_nopriv_ - admin only functionality // Initialize rate limiter: 10 requests per minute @@ -77,6 +79,10 @@ class MLF_Ajax_Handler { // Validate include_italic (boolean) $include_italic = isset($_POST['include_italic']) && $_POST['include_italic'] === '1'; + // Get version info (optional) + $font_version = isset($_POST['font_version']) ? sanitize_text_field(wp_unslash($_POST['font_version'])) : ''; + $font_last_modified = isset($_POST['font_last_modified']) ? sanitize_text_field(wp_unslash($_POST['font_last_modified'])) : ''; + // 5. PROCESS REQUEST try { $downloader = new MLF_Font_Downloader(); @@ -91,7 +97,9 @@ class MLF_Ajax_Handler { $result = $registry->register_font( $download_result['font_name'], $download_result['font_slug'], - $download_result['files'] + $download_result['files'], + $font_version, + $font_last_modified ); if (is_wp_error($result)) { @@ -170,6 +178,166 @@ class MLF_Ajax_Handler { } } + /** + * Handle check for updates AJAX request. + */ + public function handle_check_updates() { + // 1. NONCE CHECK + if (!check_ajax_referer('mlf_check_updates', 'nonce', false)) { + wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403); + } + + // 2. CAPABILITY CHECK + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403); + } + + // 3. RATE LIMIT CHECK + if (!$this->rate_limiter->check_and_record('check_updates')) { + wp_send_json_error([ + 'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'), + ], 429); + } + + // 4. GET INSTALLED FONTS AND CHECK VERSIONS + try { + $registry = new MLF_Font_Registry(); + $installed_fonts = $registry->get_imported_fonts(); + + if (empty($installed_fonts)) { + wp_send_json_success(['updates' => []]); + } + + $font_search = new MLF_Font_Search(); + $updates = []; + + foreach ($installed_fonts as $font) { + $current_version = $font_search->get_font_version($font['name']); + + if ($current_version && !empty($current_version['version'])) { + $installed_version = $font['version'] ?? ''; + + // Compare versions + if (!empty($installed_version) && $installed_version !== $current_version['version']) { + $updates[$font['id']] = [ + 'installed_version' => $installed_version, + 'latest_version' => $current_version['version'], + 'last_modified' => $current_version['lastModified'], + ]; + } elseif (empty($installed_version)) { + // No version stored, consider it needs update + $updates[$font['id']] = [ + 'installed_version' => '', + 'latest_version' => $current_version['version'], + 'last_modified' => $current_version['lastModified'], + ]; + } + } + } + + wp_send_json_success(['updates' => $updates]); + } catch (Exception $e) { + error_log('MLF Check Updates Error: ' . sanitize_text_field($e->getMessage())); + wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]); + } + } + + /** + * Handle font update (re-download) AJAX request. + */ + public function handle_update_font() { + // 1. NONCE CHECK + if (!check_ajax_referer('mlf_update_font', 'nonce', false)) { + wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403); + } + + // 2. CAPABILITY CHECK + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403); + } + + // 3. RATE LIMIT CHECK + if (!$this->rate_limiter->check_and_record('update')) { + wp_send_json_error([ + 'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'), + ], 429); + } + + // 4. INPUT VALIDATION + $font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0; + + if ($font_id < 1) { + wp_send_json_error(['message' => __('Invalid font ID.', 'maple-local-fonts')]); + } + + // Verify font exists and is one we imported + $font = get_post($font_id); + if (!$font || $font->post_type !== 'wp_font_family') { + wp_send_json_error(['message' => __('Font not found.', 'maple-local-fonts')]); + } + + if (get_post_meta($font_id, '_mlf_imported', true) !== '1') { + wp_send_json_error(['message' => __('Cannot update fonts not imported by this plugin.', 'maple-local-fonts')]); + } + + // Get font name from post content + $settings = json_decode($font->post_content, true); + $font_name = $settings['name'] ?? $font->post_title; + + // 5. DELETE OLD FONT AND RE-DOWNLOAD + try { + $registry = new MLF_Font_Registry(); + + // Delete old font + $delete_result = $registry->delete_font($font_id); + if (is_wp_error($delete_result)) { + wp_send_json_error(['message' => $this->get_user_error_message($delete_result)]); + } + + // Get latest version info + $font_search = new MLF_Font_Search(); + $version_info = $font_search->get_font_version($font_name); + $font_version = $version_info['version'] ?? ''; + $font_last_modified = $version_info['lastModified'] ?? ''; + + // Re-download font (include italic by default) + $downloader = new MLF_Font_Downloader(); + $download_result = $downloader->download($font_name, true); + + if (is_wp_error($download_result)) { + wp_send_json_error(['message' => $this->get_user_error_message($download_result)]); + } + + // Register font with new version info + $result = $registry->register_font( + $download_result['font_name'], + $download_result['font_slug'], + $download_result['files'], + $font_version, + $font_last_modified + ); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $this->get_user_error_message($result)]); + } + + wp_send_json_success([ + 'message' => sprintf( + /* translators: %s: font name */ + __('Successfully updated %s.', 'maple-local-fonts'), + esc_html($font_name) + ), + 'font_id' => $result, + 'version' => $font_version, + ]); + } catch (Exception $e) { + error_log('MLF Update Font Error: ' . sanitize_text_field($e->getMessage())); + wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]); + } + } + /** * Convert internal error codes to user-friendly messages. * diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php index c9584d6..d83f15d 100644 --- a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php @@ -25,11 +25,14 @@ class MLF_Font_Downloader { private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; /** - * All available font weights. + * Common font weights to request. + * + * Most fonts support at least 300-700. We also include 800-900 for fonts + * that support Black/Heavy weights. * * @var array */ - private $all_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900]; + private $all_weights = [300, 400, 500, 600, 700, 800, 900]; /** * Download a font from Google Fonts. @@ -151,6 +154,8 @@ class MLF_Font_Downloader { /** * Download static fonts (fallback when variable not available). * + * Tries different weight combinations since fonts support varying weights. + * * @param string $font_name Font family name. * @param bool $include_italic Whether to include italic styles. * @return array|WP_Error Download result or error. @@ -158,8 +163,28 @@ class MLF_Font_Downloader { private function download_static_fonts($font_name, $include_italic) { $styles = $include_italic ? ['normal', 'italic'] : ['normal']; - // Fetch CSS from Google - $css = $this->fetch_static_css($font_name, $this->all_weights, $styles); + // Try different weight combinations - fonts support varying weights + $weight_sets = [ + [300, 400, 500, 600, 700, 800, 900], // Full range + [300, 400, 500, 600, 700, 800], // Without 900 + [300, 400, 500, 600, 700], // Common range + [400, 500, 600, 700], // Without light + [400, 700], // Just regular and bold + ]; + + $css = null; + foreach ($weight_sets as $weights) { + $css = $this->fetch_static_css($font_name, $weights, $styles); + + if (!is_wp_error($css)) { + break; + } + + // If error is not "font not found", stop trying + if ($css->get_error_code() !== 'font_not_found') { + return $css; + } + } if (is_wp_error($css)) { return $css; @@ -191,6 +216,9 @@ class MLF_Font_Downloader { /** * Fetch variable font CSS from Google Fonts API. * + * Tries progressively smaller weight ranges until one works, + * since different fonts support different weight ranges. + * * @param string $font_name Font family name. * @param bool $italic Whether to fetch italic variant. * @return string|WP_Error CSS content or error. @@ -198,15 +226,26 @@ class MLF_Font_Downloader { private function fetch_variable_css($font_name, $italic = false) { $family = str_replace(' ', '+', $font_name); - if ($italic) { - // Request italic variable font - $url = "https://fonts.googleapis.com/css2?family={$family}:ital,wght@1,100..900&display=swap"; - } else { - // Request roman variable font - $url = "https://fonts.googleapis.com/css2?family={$family}:wght@100..900&display=swap"; + // Try different weight ranges - fonts support varying ranges + $weight_ranges = ['300..900', '300..800', '300..700', '400..700']; + + foreach ($weight_ranges as $range) { + if ($italic) { + $url = "https://fonts.googleapis.com/css2?family={$family}:ital,wght@1,{$range}&display=swap"; + } else { + $url = "https://fonts.googleapis.com/css2?family={$family}:wght@{$range}&display=swap"; + } + + $result = $this->fetch_css($url); + + // If successful or error is not "font not found", return + if (!is_wp_error($result) || $result->get_error_code() !== 'font_not_found') { + return $result; + } } - return $this->fetch_css($url); + // All ranges failed + return new WP_Error('no_variable', 'Variable font not available'); } /** diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php index 3241108..171c1f8 100644 --- a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php @@ -19,12 +19,14 @@ class MLF_Font_Registry { /** * Register a font family with WordPress Font Library. * - * @param string $font_name Display name (e.g., "Open Sans"). - * @param string $font_slug Slug (e.g., "open-sans"). - * @param array $files Array of downloaded file data. + * @param string $font_name Display name (e.g., "Open Sans"). + * @param string $font_slug Slug (e.g., "open-sans"). + * @param array $files Array of downloaded file data. + * @param string $font_version Google Fonts version (e.g., "v35"). + * @param string $last_modified Google Fonts last modified date. * @return int|WP_Error Font family post ID or error. */ - public function register_font($font_name, $font_slug, $files) { + public function register_font($font_name, $font_slug, $files, $font_version = '', $last_modified = '') { // Check if font already exists $existing = get_posts([ 'post_type' => 'wp_font_family', @@ -88,6 +90,14 @@ class MLF_Font_Registry { update_post_meta($family_id, '_mlf_imported', '1'); update_post_meta($family_id, '_mlf_import_date', current_time('mysql')); + // Store version info for update checking + if (!empty($font_version)) { + update_post_meta($family_id, '_mlf_font_version', $font_version); + } + if (!empty($last_modified)) { + update_post_meta($family_id, '_mlf_font_last_modified', $last_modified); + } + // Create font face posts (children) - WordPress also reads these foreach ($files as $file) { $filename = basename($file['path']); @@ -190,6 +200,11 @@ class MLF_Font_Registry { delete_transient('global_styles'); delete_transient('global_styles_' . get_stylesheet()); + // Clear WP_Theme_JSON caches + if (class_exists('WP_Theme_JSON_Resolver')) { + WP_Theme_JSON_Resolver::clean_cached_data(); + } + // Clear object cache for post queries wp_cache_flush_group('posts'); @@ -248,10 +263,14 @@ class MLF_Font_Registry { $faces_by_font[$parent_id][] = $face; } - // Batch get all import dates in single query + // Batch get all metadata $import_dates = []; + $font_versions = []; + $font_last_modified = []; foreach ($font_ids as $font_id) { $import_dates[$font_id] = get_post_meta($font_id, '_mlf_import_date', true); + $font_versions[$font_id] = get_post_meta($font_id, '_mlf_font_version', true); + $font_last_modified[$font_id] = get_post_meta($font_id, '_mlf_font_last_modified', true); } $result = []; @@ -285,11 +304,13 @@ class MLF_Font_Registry { }); $result[] = [ - 'id' => $font->ID, - 'name' => $settings['name'] ?? $font->post_title, - 'slug' => $settings['slug'] ?? $font->post_name, - 'variants' => $variants, - 'import_date' => $import_dates[$font->ID] ?? '', + 'id' => $font->ID, + 'name' => $settings['name'] ?? $font->post_title, + 'slug' => $settings['slug'] ?? $font->post_name, + 'variants' => $variants, + 'import_date' => $import_dates[$font->ID] ?? '', + 'version' => $font_versions[$font->ID] ?? '', + 'last_modified' => $font_last_modified[$font->ID] ?? '', ]; } diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-search.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-search.php index 59ac8e1..f52ecd3 100644 --- a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-search.php +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-search.php @@ -38,11 +38,13 @@ class MLF_Font_Search { const METADATA_URL = 'https://fonts.google.com/metadata/fonts'; /** - * Maximum metadata response size (2MB). + * Maximum metadata response size (10MB). + * + * Google Fonts metadata for 1500+ fonts can be 5-8MB. * * @var int */ - const MAX_METADATA_SIZE = 2 * 1024 * 1024; + const MAX_METADATA_SIZE = 10 * 1024 * 1024; /** * Rate limiter instance. @@ -165,10 +167,12 @@ class MLF_Font_Search { $fonts = []; foreach ($data['familyMetadataList'] as $font) { $fonts[] = [ - 'family' => $font['family'], - 'category' => $font['category'] ?? 'sans-serif', - 'variants' => $font['fonts'] ?? [], - 'subsets' => $font['subsets'] ?? ['latin'], + 'family' => $font['family'], + 'category' => $font['category'] ?? 'sans-serif', + 'variants' => $font['fonts'] ?? [], + 'subsets' => $font['subsets'] ?? ['latin'], + 'version' => $font['version'] ?? '', + 'lastModified' => $font['lastModified'] ?? '', ]; } @@ -253,6 +257,8 @@ class MLF_Font_Search { 'has_variable' => $has_variable, 'has_italic' => $has_italic, 'weights' => $weights, + 'version' => $font['version'] ?? '', + 'lastModified' => $font['lastModified'] ?? '', ]; } @@ -262,4 +268,31 @@ class MLF_Font_Search { public function clear_cache() { delete_transient(self::CACHE_KEY); } + + /** + * Get version info for a specific font. + * + * @param string $font_family Font family name. + * @return array|null Version info or null if not found. + */ + public function get_font_version($font_family) { + $fonts = $this->get_fonts_metadata(); + + if (is_wp_error($fonts)) { + return null; + } + + $font_family_lower = strtolower($font_family); + + foreach ($fonts as $font) { + if (strtolower($font['family']) === $font_family_lower) { + return [ + 'version' => $font['version'] ?? '', + 'lastModified' => $font['lastModified'] ?? '', + ]; + } + } + + return null; + } } diff --git a/native/wordpress/maple-fonts-wp/maple-local-fonts.php b/native/wordpress/maple-fonts-wp/maple-local-fonts.php index 48d0317..82cc2fe 100644 --- a/native/wordpress/maple-fonts-wp/maple-local-fonts.php +++ b/native/wordpress/maple-fonts-wp/maple-local-fonts.php @@ -151,6 +151,72 @@ function mlf_get_capability() { return apply_filters('mlf_manage_fonts_capability', 'edit_theme_options'); } +/** + * Add imported fonts to the theme.json typography settings. + * + * This makes fonts appear in the Site Editor typography dropdown. + * + * @param WP_Theme_JSON_Data $theme_json The theme.json data. + * @return WP_Theme_JSON_Data Modified theme.json data. + */ +function mlf_add_fonts_to_theme_json($theme_json) { + $registry = new MLF_Font_Registry(); + $fonts = $registry->get_imported_fonts(); + + if (empty($fonts)) { + return $theme_json; + } + + $font_families = []; + + foreach ($fonts as $font) { + $font_faces = []; + + foreach ($font['variants'] as $variant) { + $weight = $variant['weight']; + $style = $variant['style']; + + // Build filename based on our naming convention + $font_slug = sanitize_title($font['name']); + if (strpos($weight, ' ') !== false) { + // Variable font + $filename = sprintf('%s_%s_variable.woff2', $font_slug, $style); + } else { + $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight); + } + + $font_dir = wp_get_font_dir(); + $font_url = trailingslashit($font_dir['url']) . $filename; + + $font_faces[] = [ + 'fontFamily' => $font['name'], + 'fontWeight' => $weight, + 'fontStyle' => $style, + 'src' => [$font_url], + ]; + } + + $font_families[] = [ + 'name' => $font['name'], + 'slug' => $font['slug'], + 'fontFamily' => "'{$font['name']}', sans-serif", + 'fontFace' => $font_faces, + ]; + } + + $new_data = [ + 'version' => 2, + 'settings' => [ + 'typography' => [ + 'fontFamilies' => $font_families, + ], + ], + ]; + + return $theme_json->update_with($new_data); +} +add_filter('wp_theme_json_data_user', 'mlf_add_fonts_to_theme_json'); + /** * Register admin menu. */ @@ -201,36 +267,46 @@ function mlf_enqueue_admin_assets($hook) { return; } + // Use filemtime for cache-busting during development + $css_version = MLF_VERSION . '.' . filemtime(MLF_PLUGIN_DIR . 'assets/admin.css'); + $js_version = MLF_VERSION . '.' . filemtime(MLF_PLUGIN_DIR . 'assets/admin.js'); + wp_enqueue_style( 'mlf-admin', MLF_PLUGIN_URL . 'assets/admin.css', [], - MLF_VERSION + $css_version ); wp_enqueue_script( 'mlf-admin', MLF_PLUGIN_URL . 'assets/admin.js', ['jquery'], - MLF_VERSION, + $js_version, true ); wp_localize_script('mlf-admin', 'mapleLocalFontsData', [ - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'downloadNonce' => wp_create_nonce('mlf_download_font'), - 'deleteNonce' => wp_create_nonce('mlf_delete_font'), - 'searchNonce' => wp_create_nonce('mlf_search_fonts'), - 'strings' => [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'downloadNonce' => wp_create_nonce('mlf_download_font'), + 'deleteNonce' => wp_create_nonce('mlf_delete_font'), + 'searchNonce' => wp_create_nonce('mlf_search_fonts'), + 'checkUpdatesNonce' => wp_create_nonce('mlf_check_updates'), + 'updateFontNonce' => wp_create_nonce('mlf_update_font'), + 'strings' => [ 'downloading' => __('Downloading...', 'maple-local-fonts'), 'deleting' => __('Deleting...', 'maple-local-fonts'), + 'updating' => __('Updating...', 'maple-local-fonts'), + 'checking' => __('Checking...', 'maple-local-fonts'), 'confirmDelete' => __('Are you sure you want to delete this font?', 'maple-local-fonts'), 'error' => __('An error occurred. Please try again.', 'maple-local-fonts'), - 'searchPlaceholder' => __('Search Google Fonts...', 'maple-local-fonts'), 'searching' => __('Searching...', 'maple-local-fonts'), 'noResults' => __('No fonts found. Try a different search term.', 'maple-local-fonts'), - 'selectFont' => __('Select a font from the search results above.', 'maple-local-fonts'), + 'selectFont' => __('Please select a font first.', 'maple-local-fonts'), 'previewText' => __('Maple Fonts Preview', 'maple-local-fonts'), + 'minChars' => __('Please enter at least 2 characters.', 'maple-local-fonts'), + 'noUpdates' => __('All fonts are up to date.', 'maple-local-fonts'), + 'updatesFound' => __('Updates available for %d font(s).', 'maple-local-fonts'), ], ]); }