From e65a21f5fc6c5a43a8d328d68c2f17a4164c68d6 Mon Sep 17 00:00:00 2001 From: EP Date: Tue, 11 Mar 2025 23:10:47 -0400 Subject: [PATCH] Improved category jump scroll and simplified emoji decoder. Changes: 1) Added smooth category jump scroll with active state highlighting 2) Simplified emoji tab decoder to only attempt emoji decoding --- css/style.css | 23 +++- js/app.js | 226 ++++++++++++++++++++++---------- js/emojiLibrary.js | 19 +++ js/transforms.js | 64 ++++++++- node_modules/.package-lock.json | 6 + 5 files changed, 267 insertions(+), 71 deletions(-) create mode 100644 node_modules/.package-lock.json diff --git a/css/style.css b/css/style.css index 65453b3..d6360bd 100644 --- a/css/style.css +++ b/css/style.css @@ -23,12 +23,15 @@ --format-color: #ffb74d; /* Orange for formatting */ --unicode-color: #42a5f5; /* Blue for unicode transformations */ --special-color: #66bb6a; /* Green for special transformations */ + scroll-behavior: smooth; + scroll-padding-top: 20px; } * { box-sizing: border-box; margin: 0; padding: 0; + scroll-behavior: smooth; } body { @@ -38,6 +41,10 @@ body { line-height: 1.6; padding: 20px; min-height: 100vh; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: "liga" 1, "calt" 1; /* Enable ligatures */ } .container { @@ -591,6 +598,12 @@ textarea { max-height: none !important; overflow: visible !important; max-width: 100% !important; + font-family: 'Segoe UI Emoji', 'Segoe UI Symbol', 'Apple Color Emoji', + 'Noto Color Emoji', 'Android Emoji', 'EmojiOne Color', + 'Twemoji Mozilla', sans-serif !important; + /* Improve emoji rendering */ + text-rendering: optimizeLegibility !important; + -webkit-font-smoothing: antialiased !important; } .emoji-category-tabs { @@ -870,8 +883,16 @@ button:hover { border-left-style: solid; background-color: var(--button-bg); transition: all 0.2s ease; - cursor: default; + cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + user-select: none; +} + +.legend-item.active-category { + background-color: var(--accent-color); + color: var(--main-bg-color); + transform: translateY(-1px); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .legend-item:hover { diff --git a/js/app.js b/js/app.js index 5ba8436..82203d9 100644 --- a/js/app.js +++ b/js/app.js @@ -16,8 +16,8 @@ window.app = new Vue({ transformCategories: { encoding: ['Base64', 'Base32', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities'], cipher: ['Caesar Cipher', 'ROT13', 'ROT47', 'Morse Code'], - visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text'], - format: ['Pig Latin', 'Leetspeak', 'NATO Phonetic'], + visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text', 'Wingdings Style'], + format: ['Pig Latin', 'Leetspeak', 'NATO Phonetic', 'Greek Letters'], unicode: ['Invisible Text', 'Upside Down', 'Full Width', 'Small Caps', 'Bubble', 'Braille'], special: ['Medieval', 'Cursive', 'Monospace', 'Double-Struck', 'Elder Futhark', 'Mirror Text', 'Zalgo'] }, @@ -119,7 +119,18 @@ window.app = new Vue({ // Update active transform and apply it this.activeTransform = transform; - this.transformOutput = transform.func(this.transformInput); + + // Handle text with proper Unicode segmentation + const segments = window.emojiLibrary.splitEmojis(this.transformInput); + const transformedSegments = segments.map(segment => { + // Skip transformation for emojis and complex Unicode characters + if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) { + return segment; + } + return transform.func(segment); + }); + + this.transformOutput = window.emojiLibrary.joinEmojis(transformedSegments); // Set flag to mark this as a transform-initiated copy this.isTransformCopy = true; @@ -154,8 +165,17 @@ window.app = new Vue({ autoTransform() { // Only proceed if we're in the transforms tab and have an active transform if (this.transformInput && this.activeTransform && this.activeTab === 'transforms') { - // Update the output without copying - this.transformOutput = this.activeTransform.func(this.transformInput); + // Handle text with proper Unicode segmentation + const segments = window.emojiLibrary.splitEmojis(this.transformInput); + const transformedSegments = segments.map(segment => { + // Skip transformation for emojis and complex Unicode characters + if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) { + return segment; + } + return this.activeTransform.func(segment); + }); + + this.transformOutput = window.emojiLibrary.joinEmojis(transformedSegments); } }, @@ -171,8 +191,17 @@ window.app = new Vue({ } try { - // Use the transform's reverse function to decode the input - const decodedText = transform.reverse(this.transformInput); + // Handle text with proper Unicode segmentation + const segments = window.emojiLibrary.splitEmojis(this.transformInput); + const decodedSegments = segments.map(segment => { + // Skip decoding for emojis and complex Unicode characters + if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) { + return segment; + } + return transform.reverse(segment); + }); + + const decodedText = window.emojiLibrary.joinEmojis(decodedSegments); if (decodedText !== this.transformInput) { // Update the input with the decoded text @@ -461,35 +490,71 @@ window.app = new Vue({ try { // Use Clipboard API if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text) - .then(() => { - // Only show notification for transform copies - if (this.isTransformCopy) { - this.showCopiedPopup(); - - // Temporarily disable keyboard events - this.ignoreKeyboardEvents = true; - clearTimeout(this.keyboardEventsTimeout); - this.keyboardEventsTimeout = setTimeout(() => { - this.ignoreKeyboardEvents = false; - }, 1000); - } - - // Reset transform flag - this.isTransformCopy = false; - - // Focus back on input - const inputBox = document.querySelector('#transform-input'); - if (inputBox) { - inputBox.focus(); - const len = inputBox.value.length; - inputBox.setSelectionRange(len, len); - } - }) - .catch(err => { - console.warn('Clipboard API failed:', err); - this.forceFallbackCopy(text); - }); + // For emojis and complex characters, use a more robust approach + const processedText = typeof text === 'string' ? text : String(text); + + // Try to use the newer clipboard API methods if available + if (navigator.clipboard.write && processedText.match(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u)) { + const blob = new Blob([processedText], { type: 'text/plain;charset=utf-8' }); + const clipboardItem = new ClipboardItem({ 'text/plain': blob }); + navigator.clipboard.write([clipboardItem]) + .then(() => { + if (this.isTransformCopy) { + this.showCopiedPopup(); + this.ignoreKeyboardEvents = true; + clearTimeout(this.keyboardEventsTimeout); + this.keyboardEventsTimeout = setTimeout(() => { + this.ignoreKeyboardEvents = false; + }, 1000); + } + this.isTransformCopy = false; + const inputBox = document.querySelector('#transform-input'); + if (inputBox) { + inputBox.focus(); + const len = inputBox.value.length; + inputBox.setSelectionRange(len, len); + } + }) + .catch(err => { + console.warn('Advanced Clipboard API failed:', err); + // Fall back to basic writeText + navigator.clipboard.writeText(processedText) + .then(() => { + if (this.isTransformCopy) { + this.showCopiedPopup(); + } + this.isTransformCopy = false; + const inputBox = document.querySelector('#transform-input'); + if (inputBox) { + inputBox.focus(); + const len = inputBox.value.length; + inputBox.setSelectionRange(len, len); + } + }) + .catch(err => { + console.warn('Basic Clipboard API failed:', err); + this.forceFallbackCopy(processedText); + }); + }); + } else { + navigator.clipboard.writeText(processedText) + .then(() => { + if (this.isTransformCopy) { + this.showCopiedPopup(); + } + this.isTransformCopy = false; + const inputBox = document.querySelector('#transform-input'); + if (inputBox) { + inputBox.focus(); + const len = inputBox.value.length; + inputBox.setSelectionRange(len, len); + } + }) + .catch(err => { + console.warn('Basic Clipboard API failed:', err); + this.forceFallbackCopy(processedText); + }); + } } else { this.forceFallbackCopy(text); } @@ -514,9 +579,17 @@ window.app = new Vue({ // Create temporary textarea for copying const textarea = document.createElement('textarea'); textarea.value = text; + + // Ensure proper emoji rendering + textarea.style.fontFamily = "'Segoe UI Emoji', 'Apple Color Emoji', sans-serif"; + textarea.style.fontSize = '16px'; + + // Position offscreen but with proper dimensions textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; textarea.style.top = '0'; + textarea.style.width = '100px'; + textarea.style.height = '100px'; document.body.appendChild(textarea); // Focus and select the text @@ -666,11 +739,25 @@ window.app = new Vue({ universalDecode(input) { if (!input) return ''; - // Try all decoders in order + // If we're in the steganography tab, only try emoji decoding + if (this.activeTab === 'steganography') { + if (/[\u{1F300}-\u{1F6FF}\u{2600}-\u{26FF}]/u.test(input)) { + console.log('In emoji tab: attempting emoji decode only...'); + const decoded = window.steganography.decodeEmoji(input); + if (decoded) { + console.log('Successfully decoded emoji:', decoded); + return { text: decoded, method: 'Emoji Steganography' }; + } else { + console.log('No hidden message found in emoji'); + return null; + } + } + return null; + } + // For other tabs, try all decoders in order // 1. Try steganography decoders // - Check for emoji steganography first - // The emoji encoding uses variation selectors which are hard to see if (/[\u{1F300}-\u{1F6FF}\u{2600}-\u{26FF}]/u.test(input)) { console.log('Detected emoji, attempting to decode...'); const decoded = window.steganography.decodeEmoji(input); @@ -1074,38 +1161,41 @@ window.app = new Vue({ }, selectEmoji(emoji) { - this.selectedEmoji = emoji; + // Directly copy the emoji to clipboard + this.forceCopyToClipboard(emoji); + this.showNotification(` Emoji copied!`, 'success'); + this.addToCopyHistory('Emoji', emoji); - // Create a temporary carrier for this emoji - const tempCarrier = { - name: `${emoji} Carrier`, - emoji: emoji, - encode: (text) => this.steganography.encode(text, emoji), - decode: (text) => this.steganography.decode(text), - preview: (text) => `${emoji}${text}${emoji}` - }; - - // Use this emoji as carrier - this.selectedCarrier = tempCarrier; - this.activeSteg = 'emoji'; - - // Encode the message with this emoji - if (this.emojiMessage) { - this.autoEncode(); + // Also set up carrier if we're in steganography mode + if (this.activeTab === 'steganography') { + this.selectedEmoji = emoji; - // Wait for encoding to complete, then copy to clipboard - this.$nextTick(() => { - if (this.encodedMessage) { - // Force clipboard copy regardless of event source - this.forceCopyToClipboard(this.encodedMessage); - this.showNotification(` Copied hidden message with ${emoji}`, 'success'); - - // Add to copy history - this.addToCopyHistory(`Emoji: ${emoji}`, this.encodedMessage); - } - }); - } else { - this.showNotification(`Select an emoji and enter a message first`, 'info'); + // Create a temporary carrier for this emoji + const tempCarrier = { + name: `${emoji} Carrier`, + emoji: emoji, + encode: (text) => this.steganography.encode(text, emoji), + decode: (text) => this.steganography.decode(text), + preview: (text) => `${emoji}${text}${emoji}` + }; + + // Use this emoji as carrier + this.selectedCarrier = tempCarrier; + this.activeSteg = 'emoji'; + + // Encode the message with this emoji if we have one + if (this.emojiMessage) { + this.autoEncode(); + + // Wait for encoding to complete, then copy to clipboard + this.$nextTick(() => { + if (this.encodedMessage) { + this.forceCopyToClipboard(this.encodedMessage); + this.showNotification(` Hidden message copied with ${emoji}`, 'success'); + this.addToCopyHistory(`Hidden Message with ${emoji}`, this.encodedMessage); + } + }); + } } }, diff --git a/js/emojiLibrary.js b/js/emojiLibrary.js index cd5e152..d37f615 100644 --- a/js/emojiLibrary.js +++ b/js/emojiLibrary.js @@ -3,6 +3,25 @@ // Create namespace for emoji library window.emojiLibrary = {}; +// Polyfill for Intl.Segmenter if not available +if (!Intl.Segmenter) { + console.warn('Intl.Segmenter not available, falling back to basic character splitting'); +} + +// Helper function to properly split text into grapheme clusters (emojis) +window.emojiLibrary.splitEmojis = function(text) { + if (Intl.Segmenter) { + const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); + return Array.from(segmenter.segment(text), ({ segment }) => segment); + } + return Array.from(text); +}; + +// Helper function to properly join emojis +window.emojiLibrary.joinEmojis = function(emojis) { + return emojis.join(''); +}; + // Additional emojis for expanded library window.emojiLibrary.ADDITIONAL_EMOJIS = [ // Animals & Nature diff --git a/js/transforms.js b/js/transforms.js index 11e94d4..797701c 100644 --- a/js/transforms.js +++ b/js/transforms.js @@ -456,20 +456,32 @@ const transforms = { strikethrough: { name: 'Strikethrough', func: function(text) { - return [...text].map(c => c + '̶').join(''); + // Use proper Unicode combining characters for strikethrough + const segments = window.emojiLibrary.splitEmojis(text); + return segments.map(c => c + '\u0336').join(''); }, preview: function(text) { return this.func(text); + }, + reverse: function(text) { + // Remove combining strikethrough characters + return text.replace(/\u0336/g, ''); } }, underline: { name: 'Underline', func: function(text) { - return [...text].map(c => c + '̲').join(''); + // Use proper Unicode combining characters for underline + const segments = window.emojiLibrary.splitEmojis(text); + return segments.map(c => c + '\u0332').join(''); }, preview: function(text) { return this.func(text); + }, + reverse: function(text) { + // Remove combining underline characters + return text.replace(/\u0332/g, ''); } }, @@ -792,6 +804,54 @@ const transforms = { } }, + greek: { + name: 'Greek Letters', + map: { + 'A': 'Α', 'B': 'Β', 'G': 'Γ', 'D': 'Δ', 'E': 'Ε', + 'Z': 'Ζ', 'H': 'Η', 'TH': 'Θ', 'I': 'Ι', 'K': 'Κ', + 'L': 'Λ', 'M': 'Μ', 'N': 'Ν', 'X': 'Ξ', 'O': 'Ο', + 'P': 'Π', 'R': 'Ρ', 'S': 'Σ', 'T': 'Τ', 'Y': 'Υ', + 'F': 'Φ', 'CH': 'Χ', 'PS': 'Ψ', 'W': 'Ω', + 'a': 'α', 'b': 'β', 'g': 'γ', 'd': 'δ', 'e': 'ε', + 'z': 'ζ', 'h': 'η', 'th': 'θ', 'i': 'ι', 'k': 'κ', + 'l': 'λ', 'm': 'μ', 'n': 'ν', 'x': 'ξ', 'o': 'ο', + 'p': 'π', 'r': 'ρ', 's': 'σ', 't': 'τ', 'y': 'υ', + 'f': 'φ', 'ch': 'χ', 'ps': 'ψ', 'w': 'ω' + }, + func: function(text) { + return text.replace(/ch|th|ps|[a-zA-Z]/g, match => this.map[match] || match); + }, + preview: function(text) { + return this.func(text); + }, + reverse: function(text) { + const reverseMap = {}; + Object.entries(this.map).forEach(([k, v]) => reverseMap[v] = k); + return text.replace(/[Α-Ωα-ω]/g, match => reverseMap[match] || match); + } + }, + + wingdings: { + name: 'Wingdings Style', + map: { + 'a': '📧', 'b': '📎', 'c': '💿', 'd': '📁', 'e': '✉️', + 'f': '📂', 'g': '✂️', 'h': '🔨', 'i': '💡', 'j': '🔑', + 'k': '🔒', 'l': '🔓', 'm': '🖱️', 'n': '💻', 'o': '⭕', + 'p': '📱', 'q': '❓', 'r': '🌹', 's': '⭐', 't': '📌', + 'u': '☂️', 'v': '✌️', 'w': '🌊', 'x': '❌', 'y': '☯️', + 'z': '💤', '1': '☝️', '2': '✌️', '3': '🤟', '4': '4️⃣', + '5': '5️⃣', '6': '6️⃣', '7': '7️⃣', '8': '8️⃣', '9': '9️⃣', + '0': '0️⃣', '!': '❗', '?': '❓', '.': '•', ',': '،', + ' ': ' ' + }, + func: function(text) { + return text.toLowerCase().split('').map(char => this.map[char] || char).join(''); + }, + preview: function(text) { + return this.func(text); + } + }, + base32: { name: 'Base32', alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..6dc7dc3 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "P4RS3LT0NGV3", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}