font management fixes

This commit is contained in:
rodolfomartinez 2026-02-02 08:31:36 -05:00
parent 847ed92c23
commit d5ecb31dad
8 changed files with 728 additions and 99 deletions

View file

@ -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;
}

View file

@ -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('<span class="mlf-badge mlf-badge-variable">Variable</span>');
}
html += '<div class="mlf-result-item" data-font-family="' + MLF.escapeHtml(fontFamily) + '">';
html += '<div class="mlf-result-item" data-font-family="' + MLF.escapeHtml(fontFamily) + '" data-font-version="' + MLF.escapeHtml(font.version || '') + '" data-font-modified="' + MLF.escapeHtml(font.lastModified || '') + '">';
html += ' <div class="mlf-result-info">';
html += ' <span class="mlf-result-name">' + MLF.escapeHtml(fontFamily) + '</span>';
html += ' <span class="mlf-result-category">' + MLF.escapeHtml(categoryLabel) + '</span>';
@ -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('<div class="mlf-no-results">' + MLF.escapeHtml(message) + '</div>');
$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('<div class="mlf-search-error">' + MLF.escapeHtml(message) + '</div>');
$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');
}
});
}
};

View file

@ -49,15 +49,17 @@ class MLF_Admin_Page {
<div class="mlf-form-row">
<label for="mlf-font-search"><?php esc_html_e('Search Fonts', 'maple-local-fonts'); ?></label>
<div class="mlf-search-wrapper">
<span class="dashicons dashicons-search mlf-search-icon"></span>
<input type="text"
id="mlf-font-search"
class="mlf-search-input"
placeholder="<?php esc_attr_e('Search Google Fonts...', 'maple-local-fonts'); ?>"
placeholder="<?php esc_attr_e('Enter font name...', 'maple-local-fonts'); ?>"
autocomplete="off" />
<button type="button" class="button mlf-search-btn" id="mlf-search-btn">
<?php esc_html_e('Search', 'maple-local-fonts'); ?>
</button>
<span class="spinner mlf-search-spinner" id="mlf-search-spinner"></span>
</div>
<p class="description"><?php esc_html_e('Type at least 2 characters to search.', 'maple-local-fonts'); ?></p>
<p class="description"><?php esc_html_e('Enter at least 2 characters and click Search.', 'maple-local-fonts'); ?></p>
</div>
<!-- Search Results -->
@ -99,22 +101,32 @@ class MLF_Admin_Page {
<div class="mlf-info-note">
<span class="dashicons dashicons-info-outline"></span>
<span><?php esc_html_e('All font weights (100-900) will be downloaded automatically. Variable fonts are used when available for better performance.', 'maple-local-fonts'); ?></span>
<span><?php esc_html_e('Weights 300-900 (Light to Black) will be downloaded. Variable fonts are used when available, which include all weights in a single efficient file.', 'maple-local-fonts'); ?></span>
</div>
</div>
<!-- Installed Fonts Section -->
<div class="mlf-section mlf-installed-section">
<h2><?php esc_html_e('Installed Fonts', 'maple-local-fonts'); ?></h2>
<div class="mlf-section-header">
<h2><?php esc_html_e('Installed Fonts', 'maple-local-fonts'); ?></h2>
<?php if (!empty($installed_fonts)) : ?>
<button type="button" class="button mlf-check-updates-btn" id="mlf-check-updates">
<?php esc_html_e('Check for Updates', 'maple-local-fonts'); ?>
</button>
<?php endif; ?>
</div>
<?php if (empty($installed_fonts)) : ?>
<p class="mlf-no-fonts"><?php esc_html_e('No fonts installed yet. Search and select a font above to get started.', 'maple-local-fonts'); ?></p>
<?php else : ?>
<div id="mlf-font-list" class="mlf-font-list">
<?php foreach ($installed_fonts as $font) : ?>
<div class="mlf-font-item" data-font-id="<?php echo esc_attr($font['id']); ?>">
<div class="mlf-font-item" data-font-id="<?php echo esc_attr($font['id']); ?>" data-font-name="<?php echo esc_attr($font['name']); ?>" data-font-version="<?php echo esc_attr($font['version']); ?>">
<div class="mlf-font-info">
<h3 class="mlf-font-name"><?php echo esc_html($font['name']); ?></h3>
<div class="mlf-font-header">
<h3 class="mlf-font-name"><?php echo esc_html($font['name']); ?></h3>
<span class="mlf-update-badge" style="display: none;"><?php esc_html_e('Update available', 'maple-local-fonts'); ?></span>
</div>
<p class="mlf-font-variants">
<?php
$variant_strings = [];
@ -124,8 +136,22 @@ class MLF_Admin_Page {
echo esc_html(implode(', ', $variant_strings));
?>
</p>
<p class="mlf-font-meta">
<?php if (!empty($font['version'])) : ?>
<span class="mlf-font-version"><?php echo esc_html($font['version']); ?></span>
<?php endif; ?>
<?php if (!empty($font['last_modified'])) : ?>
<span class="mlf-font-modified"><?php
/* translators: %s: date */
printf(esc_html__('Updated: %s', 'maple-local-fonts'), esc_html($font['last_modified']));
?></span>
<?php endif; ?>
</p>
</div>
<div class="mlf-font-actions">
<button type="button" class="button mlf-update-btn" data-font-id="<?php echo esc_attr($font['id']); ?>" data-font-name="<?php echo esc_attr($font['name']); ?>" style="display: none;">
<?php esc_html_e('Update', 'maple-local-fonts'); ?>
</button>
<button type="button" class="button mlf-delete-btn" data-font-id="<?php echo esc_attr($font['id']); ?>">
<?php esc_html_e('Delete', 'maple-local-fonts'); ?>
</button>

View file

@ -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.
*

View file

@ -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');
}
/**

View file

@ -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] ?? '',
];
}

View file

@ -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;
}
}

View file

@ -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'),
],
]);
}