// Initialize Vue app
window.app = new Vue({
el: '#app',
data: {
// Theme
isDarkTheme: true,
// Tab Management
activeTab: 'transforms',
// Transform Tab
transformInput: '',
transformOutput: '',
activeTransform: null,
transforms: Object.entries(window.transforms).map(([key, transform]) => ({
name: transform.name,
func: transform.func.bind(transform),
preview: transform.preview.bind(transform)
})),
// Steganography Tab
emojiMessage: '',
encodedMessage: '',
decodeInput: '',
decodedMessage: '',
selectedCarrier: null,
// Universal Decoder - works on both tabs
universalDecodeInput: '',
universalDecodeResult: null,
isPasteOperation: false, // Flag to track paste operations
activeSteg: null,
carriers: window.steganography.carriers,
showDecoder: true,
// Emoji Library
filteredEmojis: [...window.emojiLibrary.EMOJI_LIST],
selectedEmoji: null,
// History of copied content
copyHistory: [],
maxHistoryItems: 10,
showCopyHistory: false
},
methods: {
// Theme Toggle
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
document.body.classList.toggle('light-theme');
},
// Copy History Toggle
toggleCopyHistory() {
this.showCopyHistory = !this.showCopyHistory;
console.log('Copy history toggled:', this.showCopyHistory);
// If showing history panel, focus the first copy-again button if available
if (this.showCopyHistory && this.copyHistory.length > 0) {
this.$nextTick(() => {
const firstCopyButton = document.querySelector('.copy-again-button');
if (firstCopyButton) {
firstCopyButton.focus();
}
});
}
},
// Transform Methods
applyTransform(transform) {
if (this.transformInput) {
this.activeTransform = transform;
this.transformOutput = transform.func(this.transformInput);
// Force copy and log to history
this.forceCopyToClipboard(this.transformOutput);
// Add to copy history
this.addToCopyHistory(`Transform: ${transform.name}`, this.transformOutput);
// Enhanced notification for transform and copy
this.showNotification(` ${transform.name} applied and copied!`, 'success');
}
},
autoTransform() {
// Only proceed if we're in the transforms tab and have an active transform
if (this.transformInput && this.activeTransform && this.activeTab === 'transforms') {
this.transformOutput = this.activeTransform.func(this.transformInput);
// Use forceCopyToClipboard for auto-copy
this.forceCopyToClipboard(this.transformOutput);
// Add to copy history
this.addToCopyHistory(`Transform: ${this.activeTransform.name}`, this.transformOutput);
}
},
// Steganography Methods
selectCarrier(carrier) {
// Toggle carrier selection if clicking the same one again
if (this.selectedCarrier === carrier) {
this.selectedCarrier = null;
this.encodedMessage = '';
} else {
this.selectedCarrier = carrier;
this.activeSteg = 'emoji';
this.autoEncode();
}
},
setStegMode(mode) {
// For invisible text, make it a direct action (not a toggle)
if (mode === 'invisible') {
// Set the mode temporarily to generate the encoded message
this.activeSteg = mode;
// Clear any carrier selection
this.selectedCarrier = null;
// Generate the encoded message
this.autoEncode();
// Auto-copy the encoded message
if (this.encodedMessage) {
this.$nextTick(() => {
this.forceCopyToClipboard(this.encodedMessage);
this.showNotification(' Invisible text created and copied!', 'success');
this.addToCopyHistory('Invisible Text', this.encodedMessage);
});
}
} else {
// For other modes (like emoji), keep the toggle behavior
if (this.activeSteg === mode) {
this.activeSteg = null;
this.encodedMessage = '';
} else {
this.activeSteg = mode;
this.autoEncode();
}
}
},
autoEncode() {
// Only proceed if we're in the steganography tab
if (!this.emojiMessage || this.activeTab !== 'steganography') {
this.encodedMessage = '';
return;
}
if (this.activeSteg === 'invisible') {
this.encodedMessage = window.steganography.encodeInvisible(this.emojiMessage);
// Auto-copy will be handled in setStegMode method
} else if (this.selectedCarrier) {
this.encodedMessage = window.steganography.encodeEmoji(
this.selectedCarrier.emoji,
this.emojiMessage
);
// Auto-copy for emoji carrier is handled in selectEmoji method
}
},
autoDecode() {
if (!this.decodeInput) {
this.decodedMessage = '';
return;
}
// Use the universal decoder
const result = this.universalDecode(this.decodeInput);
if (result) {
this.decodedMessage = `Decoded (${result.method}): ${result.text}`;
// Auto-copy decoded message to clipboard
this.$nextTick(() => {
// Only copy the actual decoded text, not the formatted message
const decodedText = result.text;
if (decodedText) {
// Force clipboard copy regardless of event source
this.forceCopyToClipboard(decodedText);
this.showNotification(` Decoded message copied!`, 'success');
// Add to copy history
this.addToCopyHistory(`Decoded (${result.method})`, decodedText);
}
});
} else {
this.decodedMessage = 'No encoded message detected';
}
},
previewInvisible(text) {
return '[invisible]';
},
// Add to copy history functionality
addToCopyHistory(source, content) {
// Create history item with timestamp
const historyItem = {
source: source,
content: content,
timestamp: new Date().toLocaleTimeString(),
date: new Date().toLocaleDateString()
};
// Add to beginning of array (most recent first)
this.copyHistory.unshift(historyItem);
// Limit history to maxHistoryItems
if (this.copyHistory.length > this.maxHistoryItems) {
this.copyHistory.pop();
}
// Log history item for debugging
console.log('Added to copy history:', historyItem);
},
// Utility Methods
async copyToClipboard(text) {
if (!text) return;
// Always try to copy, regardless of event source
try {
await navigator.clipboard.writeText(text);
// Show a success notification
this.showNotification(' Copied!', 'success');
// Add to history - determine source from active tab or context
const source = this.activeTab === 'transforms' ? 'Transform' : 'Steganography';
this.addToCopyHistory(source, text);
} catch (err) {
console.warn('Clipboard access not available:', err);
// Try fallback method for copying (textarea method)
this.fallbackCopy(text);
}
},
fallbackCopy(text) {
try {
// Create temporary textarea
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed'; // Avoid scrolling to bottom
document.body.appendChild(textarea);
textarea.select();
// Try the copy command
const successful = document.execCommand('copy');
// Show appropriate notification
if (successful) {
this.showNotification(' Copied!', 'success');
// Add to history when successful
// Try to determine a more specific source based on the context
let source = this.activeTab === 'transforms' ? 'Transform' : 'Steganography';
// Add more context if available
if (this.activeTab === 'transforms' && this.activeTransform) {
source = `Transform: ${this.activeTransform.name}`;
} else if (this.activeTab === 'steganography') {
if (this.activeSteg === 'invisible') {
source = 'Invisible Text';
} else if (this.selectedEmoji) {
source = `Emoji: ${this.selectedEmoji}`;
}
}
this.addToCopyHistory(source, text);
} else {
this.showNotification(' Copy not supported', 'error');
}
// Clean up
document.body.removeChild(textarea);
} catch (err) {
console.warn('Fallback copy method failed:', err);
this.showNotification(' Copy not supported', 'error');
}
},
// Force copy to clipboard regardless of event context
forceCopyToClipboard(text) {
if (!text) return;
// Skip notifications if this was triggered by a paste operation
if (this.isPasteOperation) {
console.log('Skipping clipboard notification for paste operation');
return;
}
console.log('Force copying to clipboard:', text);
try {
// Try to use the Clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(() => {
console.log('Force copy successful using Clipboard API');
// Show clear notification on success
this.showCopiedPopup();
})
.catch(err => {
console.warn('Force Clipboard API failed:', err);
this.forceFallbackCopy(text);
// Still show notification, as fallback might work
this.showCopiedPopup();
});
} else {
// Fall back to execCommand method
this.forceFallbackCopy(text);
// Show notification for fallback method too
this.showCopiedPopup();
}
} catch (error) {
console.error('Force copy failed:', error);
// Try one last fallback and still show notification
this.forceFallbackCopy(text);
this.showCopiedPopup();
}
},
// Fallback copy method that doesn't rely on user-initiated events
forceFallbackCopy(text) {
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '0';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand('copy');
console.log('Force fallback copy successful');
} catch (err) {
console.error('Force fallback copy command failed:', err);
}
document.body.removeChild(textarea);
} catch (err) {
console.error('Force fallback copy method failed:', err);
}
},
// Notification system
showNotification(message, type = 'success') {
// Create notification element
const notification = document.createElement('div');
notification.className = `copy-notification ${type}`;
notification.innerHTML = message;
document.body.appendChild(notification);
// Remove after animation
setTimeout(() => {
notification.classList.add('fade-out');
setTimeout(() => {
if (notification.parentNode) {
document.body.removeChild(notification);
}
}, 300);
}, 1000);
},
// Special prominent copy notification
showCopiedPopup() {
// Create a more visible popup just for copy operations
const popup = document.createElement('div');
popup.className = 'copy-popup';
popup.innerHTML = ' Copied to clipboard!';
// Add to body
document.body.appendChild(popup);
// Force it to be visible and centered
popup.style.position = 'fixed';
popup.style.top = '50%';
popup.style.left = '50%';
popup.style.transform = 'translate(-50%, -50%)';
popup.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
popup.style.color = 'white';
popup.style.padding = '15px 25px';
popup.style.borderRadius = '5px';
popup.style.fontSize = '18px';
popup.style.fontWeight = 'bold';
popup.style.zIndex = '10000';
popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
popup.style.textAlign = 'center';
// Add fade-in animation
popup.style.opacity = '0';
popup.style.transition = 'opacity 0.3s ease-in-out';
// Force reflow to make animation work
void popup.offsetWidth;
// Fade in
popup.style.opacity = '1';
// Remove after a short delay
setTimeout(() => {
popup.style.opacity = '0';
setTimeout(() => {
if (popup.parentNode) {
document.body.removeChild(popup);
}
}, 300);
}, 1500);
},
// Run the universal decoder when input changes
runUniversalDecode() {
console.log('Running universal decoder with input:', this.universalDecodeInput);
// Clear result if input is empty
if (!this.universalDecodeInput) {
this.universalDecodeResult = null;
return;
}
// Try to decode using all available methods
const result = this.universalDecode(this.universalDecodeInput);
// Update the result
this.universalDecodeResult = result;
// Log the result
if (result) {
console.log(`Universal decoder found a match: ${result.method}`);
} else {
console.log('Universal decoder could not decode the input');
}
},
// Universal Decoder - tries all decoding methods
universalDecode(input) {
if (!input) return '';
// 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);
if (decoded) {
console.log('Successfully decoded emoji:', decoded);
return { text: decoded, method: 'Emoji Steganography' };
} else {
console.log('Emoji detected but no hidden message found');
}
}
// - Invisible text
let decoded = window.steganography.decodeInvisible(input);
if (decoded) {
return { text: decoded, method: 'Invisible Text' };
}
// 2. Try transform reversals
// - Binary
if (/^[01\s]+$/.test(input.trim())) {
try {
// Use binary transform's reverse function if available
if (window.transforms.binary && window.transforms.binary.reverse) {
const result = window.transforms.binary.reverse(input);
if (result && /[\x20-\x7E]/.test(result)) { // Make sure it's readable ASCII
return { text: result, method: 'Binary' };
}
} else {
// Fallback implementation
const binText = input.replace(/\s+/g, '');
let result = '';
for (let i = 0; i < binText.length; i += 8) {
const byte = binText.substr(i, 8);
if (byte.length === 8) {
result += String.fromCharCode(parseInt(byte, 2));
}
}
if (result && /[\x20-\x7E]/.test(result)) { // Make sure it's readable ASCII
return { text: result, method: 'Binary' };
}
}
} catch (e) {
console.error('Binary decode error:', e);
}
}
// - Morse code
if (/^[.\-\s\/]+$/.test(input.trim())) {
try {
// Use morse transform's reverse function if available
if (window.transforms.morse && window.transforms.morse.reverse) {
const result = window.transforms.morse.reverse(input);
if (result !== input && /[a-zA-Z0-9]/.test(result)) {
return { text: result, method: 'Morse Code' };
}
}
} catch (e) {
console.error('Morse decode error:', e);
}
}
// - Braille
const braillePattern = /[⠀-⣿]/;
if (braillePattern.test(input)) {
try {
// Create a reverse mapping for braille
const brailleReverseMap = {};
if (window.transforms.braille && window.transforms.braille.map) {
for (const [key, value] of Object.entries(window.transforms.braille.map)) {
brailleReverseMap[value] = key;
}
// Decode the braille
let result = '';
for (const char of input) {
result += brailleReverseMap[char] || char;
}
if (result !== input && /[a-zA-Z0-9]/.test(result)) {
return { text: result, method: 'Braille' };
}
}
} catch (e) {
console.error('Braille decode error:', e);
}
}
// - Base64
if (/^[A-Za-z0-9+/=]+$/.test(input.trim())) {
try {
// Attempt to decode as base64
const result = atob(input.trim());
// Check if result is readable text
if (/[\x20-\x7E]{3,}/.test(result)) { // At least 3 readable ASCII chars
return { text: result, method: 'Base64' };
}
} catch (e) {
// Not valid base64, continue to next decoder
console.error('Base64 decode error:', e);
}
}
// - Upside Down text
if (window.transforms.upside_down && window.transforms.upside_down.reverse) {
try {
const result = window.transforms.upside_down.reverse(input);
// Check if the result is significantly different
if (result !== input && result.length > 3 && /[a-zA-Z0-9\s]{3,}/.test(result)) {
return { text: result, method: 'Upside Down' };
}
} catch (e) {
console.error('Upside Down decode error:', e);
}
}
// - Small Caps (create reverse mapping since there's no built-in decoder)
if (window.transforms.small_caps && window.transforms.small_caps.map) {
try {
// Create reverse mapping
const smallCapsReverseMap = {};
for (const [key, value] of Object.entries(window.transforms.small_caps.map)) {
smallCapsReverseMap[value] = key;
}
// Check if input contains small caps characters
const smallCapsChars = Object.values(window.transforms.small_caps.map);
const hasSmallCaps = smallCapsChars.some(char => input.includes(char));
if (hasSmallCaps) {
// Decode text
let result = '';
for (const char of input) {
result += smallCapsReverseMap[char] || char;
}
if (result !== input && /[a-zA-Z]/.test(result)) {
return { text: result, method: 'Small Caps' };
}
}
} catch (e) {
console.error('Small Caps decode error:', e);
}
}
// - Bubble text (create reverse mapping)
if (window.transforms.bubble && window.transforms.bubble.map) {
try {
// Create reverse mapping
const bubbleReverseMap = {};
for (const [key, value] of Object.entries(window.transforms.bubble.map)) {
bubbleReverseMap[value] = key;
}
// Check if input contains bubble characters
const bubbleChars = Object.values(window.transforms.bubble.map);
const hasBubbleChars = bubbleChars.some(char => input.includes(char));
if (hasBubbleChars) {
// Decode text
let result = '';
for (const char of input) {
result += bubbleReverseMap[char] || char;
}
if (result !== input && /[a-zA-Z]/.test(result)) {
return { text: result, method: 'Bubble' };
}
}
} catch (e) {
console.error('Bubble decode error:', e);
}
}
// - Try reverse each transform that has a built-in reverse function
for (const name in window.transforms) {
const transform = window.transforms[name];
if (transform.reverse) {
try {
const result = transform.reverse(input);
// Only return if the result is different and contains readable characters
if (result !== input && /[a-zA-Z0-9\s]{3,}/.test(result)) {
return { text: result, method: transform.name };
}
} catch (e) {
console.error(`Error decoding with ${name}:`, e);
}
}
}
return null;
},
// Emoji Library Methods
filterEmojis() {
// Always show all emojis - search functionality removed
this.filteredEmojis = [...window.emojiLibrary.EMOJI_LIST];
this.renderEmojiGrid();
},
selectEmoji(emoji) {
this.selectedEmoji = 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();
// 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');
}
},
renderEmojiGrid() {
console.log('renderEmojiGrid called with', this.filteredEmojis.length, 'emojis');
// Make sure container exists
const container = document.getElementById('emoji-grid-container');
if (!container) {
console.error('emoji-grid-container not found!');
return;
}
// Force container to be completely visible
container.style.cssText = 'display: block !important; visibility: visible !important; min-height: 300px;';
// Make sure parent containers are visible too
const emojiLibrary = document.querySelector('.emoji-library');
if (emojiLibrary) {
emojiLibrary.style.cssText = 'display: block !important; visibility: visible !important;';
}
// Clear any existing content to avoid duplication
container.innerHTML = '';
// Render the emoji grid
window.emojiLibrary.renderEmojiGrid('emoji-grid-container', this.selectEmoji.bind(this), this.filteredEmojis);
// Add a bold message about copying
const copyNote = document.createElement('div');
copyNote.style.cssText = 'text-align: center; margin-top: 10px; font-weight: bold; padding: 5px; background-color: #f0f0f0; border-radius: 4px;';
copyNote.innerHTML = ' Clicking an emoji will automatically copy your hidden message';
container.appendChild(copyNote);
// Log success
console.log('Emoji grid rendered successfully');
}
},
// Initialize theme and components
mounted() {
console.log('Vue app mounted');
// Apply theme
if (this.isDarkTheme) {
document.body.classList.add('dark-theme');
}
// Initialize emoji grid with all emojis shown by default
this.$nextTick(() => {
console.log('nextTick: Initializing emoji grid');
// Make sure filtered emojis is populated
this.filteredEmojis = [...window.emojiLibrary.EMOJI_LIST];
// Define a function to properly initialize the emoji grid
const initializeEmojiGrid = () => {
const emojiGridContainer = document.getElementById('emoji-grid-container');
if (emojiGridContainer) {
console.log('Found emoji-grid-container, rendering grid');
// Set inline styles to ensure visibility
emojiGridContainer.setAttribute('style', 'display: block !important; visibility: visible !important; min-height: 300px; padding: 10px; border: 1px solid #ccc;');
// Also make sure the parent container is visible
const emojiLibrary = document.querySelector('.emoji-library');
if (emojiLibrary) {
emojiLibrary.setAttribute('style', 'display: block !important; visibility: visible !important; margin-top: 30px; border: 3px solid var(--accent-color); overflow: visible;');
}
// Now render the grid
this.renderEmojiGrid();
console.log('Emoji grid rendering complete in mounted()');
} else {
console.error('emoji-grid-container not found, will retry');
// Try again after a longer delay if not found
setTimeout(initializeEmojiGrid, 500);
}
};
// Start with a small delay to ensure DOM is ready
setTimeout(initializeEmojiGrid, 100);
// Set up paste event handlers for all textareas to prevent unwanted clipboard notifications
this.setupPasteHandlers();
});
},
// Set up paste event handlers for all textareas
setupPasteHandlers() {
// Get all textareas in the app
const textareas = document.querySelectorAll('textarea');
// Add paste event listener to each textarea
textareas.forEach(textarea => {
textarea.addEventListener('paste', (e) => {
// Mark this as an explicit paste event
this.isPasteOperation = true;
// Reset the flag after a short delay
setTimeout(() => {
this.isPasteOperation = false;
}, 100);
});
});
},
// No keyboard shortcuts - they were removed as requested
created() {
// Initialize any required functionality
// But no keyboard shortcuts/hotkeys for now
},
// Watch for input events and ensure emojis stay visible
watch: {
// Make sure emoji list stays loaded when user types in any input
emojiMessage() {
// Reset the filtered emojis to the full list whenever typing occurs
this.filteredEmojis = [...window.emojiLibrary.EMOJI_LIST];
this.$nextTick(() => {
this.renderEmojiGrid();
});
},
// Also watch the decode input field for typing activity
decodeInput() {
// Always show full emoji list
this.filteredEmojis = [...window.emojiLibrary.EMOJI_LIST];
this.$nextTick(() => {
this.renderEmojiGrid();
});
}
}
});