mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-05-26 18:07:51 +02:00
dc10a90851
- Implement tool registry system with individual tool modules - Reorganize transformers into categorized source modules - Remove emojiLibrary.js, consolidate into EmojiUtils and emojiData - Fix mobile close button and tooltip functionality - Add build system for transforms and emoji data - Migrate from Python backend to pure JavaScript - Add comprehensive documentation and testing - Improve code organization and maintainability - Ignore generated files (transforms-bundle.js, emojiData.js)
209 lines
6.7 KiB
JavaScript
209 lines
6.7 KiB
JavaScript
/**
|
|
* Emoji Compatibility Checker
|
|
* Tests which emoji features the user's browser/device supports
|
|
*/
|
|
|
|
window.emojiCompatibility = {
|
|
// Cache key for localStorage
|
|
CACHE_KEY: 'emojiTestResults_v2_simple', // Simple pixel detection only
|
|
CACHE_EXPIRY_DAYS: 30,
|
|
|
|
// In-memory cache for emoji test results
|
|
_emojiTestCache: null,
|
|
|
|
/**
|
|
* Load emoji test cache from localStorage
|
|
*/
|
|
loadCache: function() {
|
|
if (this._emojiTestCache) return this._emojiTestCache;
|
|
|
|
try {
|
|
const cached = localStorage.getItem(this.CACHE_KEY);
|
|
if (!cached) return null;
|
|
|
|
const data = JSON.parse(cached);
|
|
|
|
// Check if cache is expired
|
|
const now = Date.now();
|
|
const age = now - data.timestamp;
|
|
const maxAge = this.CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
|
|
|
|
if (age > maxAge) {
|
|
localStorage.removeItem(this.CACHE_KEY);
|
|
return null;
|
|
}
|
|
|
|
this._emojiTestCache = data.results;
|
|
return this._emojiTestCache;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Save emoji test results to localStorage
|
|
* (Called after testing all emojis)
|
|
*/
|
|
saveCache: function() {
|
|
if (!this._emojiTestCache) return;
|
|
|
|
try {
|
|
const data = {
|
|
timestamp: Date.now(),
|
|
results: this._emojiTestCache
|
|
};
|
|
localStorage.setItem(this.CACHE_KEY, JSON.stringify(data));
|
|
} catch (e) {
|
|
console.warn('⚠️ Could not save emoji test cache:', e);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear the emoji test cache (useful for debugging or forcing refresh)
|
|
*/
|
|
clearCache: function() {
|
|
localStorage.removeItem(this.CACHE_KEY);
|
|
this._emojiTestCache = null;
|
|
},
|
|
|
|
/**
|
|
* Test if a specific emoji actually renders in the browser
|
|
* Uses canvas pixel detection - the definitive test for visual rendering
|
|
*/
|
|
testEmojiRenders: function(emoji) {
|
|
// Load cache if not already loaded
|
|
if (!this._emojiTestCache) {
|
|
this._emojiTestCache = this.loadCache() || {};
|
|
}
|
|
|
|
// Check cache first
|
|
if (emoji in this._emojiTestCache) {
|
|
return this._emojiTestCache[emoji];
|
|
}
|
|
|
|
// Cache canvas for performance
|
|
if (!this._testCanvas) {
|
|
this._testCanvas = document.createElement('canvas');
|
|
this._testCanvas.width = 64;
|
|
this._testCanvas.height = 64;
|
|
// Set willReadFrequently for better performance with multiple getImageData calls
|
|
this._testCtx = this._testCanvas.getContext('2d', { willReadFrequently: true });
|
|
}
|
|
|
|
const ctx = this._testCtx;
|
|
// Use emoji font to ensure missing emojis render as boxes
|
|
ctx.font = '48px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "EmojiOne Color", "Android Emoji", sans-serif';
|
|
ctx.textBaseline = 'top';
|
|
ctx.textAlign = 'left';
|
|
|
|
// Width test - catches multi-character fallbacks like "???"
|
|
const emojiWidth = ctx.measureText(emoji).width;
|
|
const referenceWidth = ctx.measureText('😊').width;
|
|
|
|
// If emoji is much wider than a single emoji, it's likely broken into multiple chars
|
|
if (emojiWidth > referenceWidth * 1.8) {
|
|
this._emojiTestCache[emoji] = false;
|
|
return false;
|
|
}
|
|
|
|
// Pixel detection - does the emoji actually render visually?
|
|
ctx.clearRect(0, 0, 64, 64);
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillText(emoji, 8, 8);
|
|
|
|
const imageData = ctx.getImageData(0, 0, 64, 64).data;
|
|
|
|
// Check if any pixels were drawn (alpha channel > 0)
|
|
let hasPixels = false;
|
|
for (let i = 0; i < imageData.length; i += 4) {
|
|
if (imageData[i + 3] > 0) {
|
|
hasPixels = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Cache and return result
|
|
this._emojiTestCache[emoji] = hasPixels;
|
|
return hasPixels;
|
|
},
|
|
|
|
/**
|
|
* Check if a specific emoji should be shown in the UI picker
|
|
* based on browser compatibility
|
|
*/
|
|
shouldShowInPicker: function(emoji, data) {
|
|
// Simple check: Does it actually render?
|
|
// This single test catches all broken emojis regardless of type
|
|
return this.testEmojiRenders(emoji);
|
|
},
|
|
|
|
/**
|
|
* Get compatible emojis from a list (batch testing with progress callback)
|
|
* @param {Array<string>} allEmojis - Full list of emojis to test
|
|
* @param {Function} progressCallback - Optional callback (tested, total, compatible)
|
|
* @returns {Promise<Array<string>>} - Array of compatible emojis
|
|
*/
|
|
getCompatibleEmojis: async function(allEmojis, progressCallback) {
|
|
// Load cache first
|
|
this.loadCache();
|
|
|
|
const compatible = [];
|
|
let tested = 0;
|
|
const total = allEmojis.length;
|
|
|
|
// Test emojis in batches to avoid blocking
|
|
const batchSize = 50;
|
|
|
|
function testBatch() {
|
|
return new Promise((resolve) => {
|
|
const end = Math.min(tested + batchSize, total);
|
|
|
|
for (let i = tested; i < end; i++) {
|
|
const emoji = allEmojis[i];
|
|
if (this.shouldShowInPicker(emoji)) {
|
|
compatible.push(emoji);
|
|
}
|
|
tested++;
|
|
}
|
|
|
|
// Report progress
|
|
if (progressCallback) {
|
|
progressCallback(tested, total, compatible.length);
|
|
}
|
|
|
|
// Continue or finish
|
|
if (tested < total) {
|
|
requestAnimationFrame(() => {
|
|
setTimeout(() => resolve(testBatch.call(this)), 10);
|
|
});
|
|
} else {
|
|
// Save cache when done
|
|
this.saveCache();
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
await testBatch.call(this);
|
|
return compatible;
|
|
},
|
|
|
|
/**
|
|
* Get compatibility stats
|
|
*/
|
|
getStats: function() {
|
|
const cache = this.loadCache();
|
|
if (cache) {
|
|
const compatible = Object.values(cache).filter(v => v === true).length;
|
|
const total = Object.keys(cache).length;
|
|
return {
|
|
compatible: compatible,
|
|
total: total,
|
|
percentage: total > 0 ? ((compatible / total) * 100).toFixed(1) : 0
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|