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

This commit is contained in:
EP
2025-03-11 23:10:47 -04:00
parent 047e914f22
commit e65a21f5fc
5 changed files with 267 additions and 71 deletions
+22 -1
View File
@@ -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 {
+158 -68
View File
@@ -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(`<i class="fas fa-check"></i> 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(`<i class="fas fa-check"></i> 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(`<i class="fas fa-check"></i> Hidden message copied with ${emoji}`, 'success');
this.addToCopyHistory(`Hidden Message with ${emoji}`, this.encodedMessage);
}
});
}
}
},
+19
View File
@@ -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
+62 -2
View File
@@ -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',
+6
View File
@@ -0,0 +1,6 @@
{
"name": "P4RS3LT0NGV3",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}