From 27d63583a52d52fa2e658356cedc8416a879d780 Mon Sep 17 00:00:00 2001 From: Dustin Farley Date: Fri, 5 Dec 2025 23:01:07 -0800 Subject: [PATCH] favs, last used, sortable columns, and update splitter transforms to match selection --- css/style.css | 99 +++++++++++++- js/tools/SplitterTool.js | 117 ++++++++++++++++- js/tools/TransformTool.js | 266 +++++++++++++++++++++++++++++++++++++- templates/splitter.html | 22 +++- templates/transforms.html | 100 +++++++++++++- 5 files changed, 584 insertions(+), 20 deletions(-) diff --git a/css/style.css b/css/style.css index 2b19c25..b20a4eb 100644 --- a/css/style.css +++ b/css/style.css @@ -1445,6 +1445,66 @@ h1, h2, h3, h4, h5 { padding-left: 8px; text-transform: capitalize; border-left: 4px solid; + display: flex; + align-items: center; + gap: 8px; +} + +.category-order-controls { + display: flex; + gap: 4px; + margin-left: auto; + opacity: 0.6; + transition: opacity 0.2s ease; +} + +.category-title:hover .category-order-controls { + opacity: 1; +} + +.category-move-btn { + background: var(--button-bg); + border: 1px solid var(--input-border); + border-radius: 4px; + padding: 4px 6px; + cursor: pointer; + color: var(--text-color); + font-size: 0.75rem; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 24px; +} + +.category-move-btn:hover { + background: var(--button-hover-bg); + border-color: var(--accent-color); + color: var(--accent-color); + transform: scale(1.1); +} + +.category-move-btn:active { + transform: scale(0.95); +} + +.last-used-section { + border: 2px dashed var(--accent-color); + background: rgba(var(--accent-color-rgb), 0.05); +} + +.last-used-section .category-title { + color: var(--accent-color); + border-left-color: var(--accent-color); +} + +.last-used-section .transform-buttons .transform-button-group { + width: 100%; +} + +.legend-item { + position: relative; } .category-title.transform-category-encoding { @@ -1509,6 +1569,10 @@ h1, h2, h3, h4, h5 { width: 100%; } +#category-randomizer .transform-buttons .transform-button { + width: 100%; +} + #category-randomizer .transform-buttons .transform-button .transform-preview { max-width: 300px; } @@ -2083,20 +2147,47 @@ h1, h2, h3, h4, h5 { width: 30px; } -/* Auto-copy icon styling */ -.auto-copy-icon { +/* Favorite icon styling */ +.favorite-icon { position: absolute; right: 8px; bottom: 8px; font-size: 0.8rem; opacity: 0.5; transition: all 0.2s ease; + cursor: pointer; + z-index: 10; + color: var(--text-color); } -.transform-button:hover .auto-copy-icon { +.favorite-icon:hover { opacity: 1; transform: scale(1.2); - color: var(--accent-color); + color: #ffd700; +} + +.favorite-icon.favorited { + opacity: 1; + color: #ffd700; +} + +.favorite-icon.favorited:hover { + transform: scale(1.3); + color: #ffed4e; +} + +.favorites-section { + border: 2px dashed #ffd700; + background: rgba(255, 215, 0, 0.05); +} + +.favorites-section .category-title { + color: #ffd700; + border-left-color: #ffd700; +} + +.favorites-section .transform-buttons .transform-button-group { + width: 100%; } /* Output section */ diff --git a/js/tools/SplitterTool.js b/js/tools/SplitterTool.js index 97f4484..28d144a 100644 --- a/js/tools/SplitterTool.js +++ b/js/tools/SplitterTool.js @@ -13,6 +13,12 @@ class SplitterTool extends Tool { } getVueData() { + // Load favorites + const favorites = this.loadFavorites(); + + // Load category order (same as TransformTool) + const categoryOrder = this.getCategoryOrder(); + return { // Message Splitter Tab splitterInput: '', @@ -26,12 +32,121 @@ class SplitterTool extends Tool { splitterTransforms: [''], // array of transform names to apply in sequence (start with one empty slot) splitterStartWrap: '', splitterEndWrap: '', - splitMessages: [] + splitMessages: [], + favorites: favorites, + categoryOrder: categoryOrder }; } + getCategoryOrder() { + // Get all categories from transforms + if (!window.transforms) return []; + + const categorySet = new Set(); + Object.values(window.transforms).forEach(transform => { + if (transform.category) { + categorySet.add(transform.category); + } + }); + + const allCategories = Array.from(categorySet); + const savedOrder = this.loadCategoryOrder(); + + return this.mergeCategoryOrder(allCategories, savedOrder); + } + + loadCategoryOrder() { + try { + const saved = localStorage.getItem('transformCategoryOrder'); + if (saved) { + return JSON.parse(saved); + } + } catch (e) { + console.warn('Failed to load category order:', e); + } + return null; + } + + mergeCategoryOrder(allCategories, savedOrder) { + // Always ensure randomizer is last + const categoriesWithoutRandomizer = allCategories.filter(c => c !== 'randomizer'); + + if (!savedOrder || savedOrder.length === 0) { + // Default: alphabetical, randomizer last + const sorted = categoriesWithoutRandomizer.sort((a, b) => a.localeCompare(b)); + return [...sorted, 'randomizer']; + } + + // Use saved order, but filter out categories that no longer exist and remove duplicates + const validSavedOrder = savedOrder + .filter(cat => allCategories.includes(cat)) + .filter((cat, index, arr) => arr.indexOf(cat) === index); // Remove duplicates + + // Find new categories not in saved order + const newCategories = categoriesWithoutRandomizer.filter(cat => !validSavedOrder.includes(cat)); + + // Build final order: saved order (filtered, deduplicated) + new categories (alphabetically) + randomizer + const finalOrder = [...validSavedOrder]; + if (newCategories.length > 0) { + finalOrder.push(...newCategories.sort((a, b) => a.localeCompare(b))); + } + + // Ensure randomizer is always last and remove any duplicates + const finalWithoutRandomizer = finalOrder.filter(c => c !== 'randomizer'); + const uniqueFinal = finalWithoutRandomizer.filter((cat, index, arr) => arr.indexOf(cat) === index); + return [...uniqueFinal, 'randomizer']; + } + + loadFavorites() { + try { + const saved = localStorage.getItem('transformFavorites'); + if (saved) { + const data = JSON.parse(saved); + // Filter to only include transforms that still exist + if (window.transforms) { + return data.filter(transformName => { + return Object.values(window.transforms).some(t => t.name === transformName); + }); + } + } + } catch (e) { + console.warn('Failed to load favorites:', e); + } + return []; + } + getVueMethods() { return { + /** + * Get favorite transforms + */ + getFavoriteTransforms: function() { + if (!this.favorites || this.favorites.length === 0) { + return []; + } + return this.favorites + .map(transformName => { + return this.transforms.find(t => t.name === transformName); + }) + .filter(t => t !== undefined); + }, + /** + * Get transforms by category (excluding favorites) + */ + getTransformsByCategory: function(category) { + const categoryTransforms = this.transforms.filter(t => t.category === category); + // Exclude favorites from category lists (they're shown separately) + if (!this.favorites || this.favorites.length === 0) { + return categoryTransforms; + } + return categoryTransforms.filter(t => !this.favorites.includes(t.name)); + }, + /** + * Get display name for category (capitalized) + */ + getCategoryDisplayName: function(category) { + return category.charAt(0).toUpperCase() + category.slice(1); + }, /** * Set encapsulation start and end strings * @param {string} start - The start string diff --git a/js/tools/TransformTool.js b/js/tools/TransformTool.js index f8965eb..01fe46b 100644 --- a/js/tools/TransformTool.js +++ b/js/tools/TransformTool.js @@ -30,22 +30,145 @@ class TransformTool extends Tool { } }); - // Sort categories, but always put randomizer last - const sortedCategories = Array.from(categorySet).sort((a, b) => { - if (a === 'randomizer') return 1; - if (b === 'randomizer') return -1; - return a.localeCompare(b); - }); + // Legend categories: always alphabetical (for quick link buttons) + const allCategories = Array.from(categorySet); + const categoriesWithoutRandomizer = allCategories.filter(c => c !== 'randomizer'); + const legendCategories = [...categoriesWithoutRandomizer.sort((a, b) => a.localeCompare(b)), 'randomizer']; + + // Section categories: can be reordered (load saved order or use alphabetical) + const savedOrder = this.loadCategoryOrder(); + const sectionCategories = savedOrder && savedOrder.length > 0 + ? this.mergeCategoryOrder(allCategories, savedOrder) + : [...legendCategories]; // Create a copy so legendCategories remains immutable + + // Load last used transforms + const lastUsed = this.loadLastUsed(); + + // Load favorites + const favorites = this.loadFavorites(); return { transformInput: '', transformOutput: '', activeTransform: null, transforms: transforms, - categories: sortedCategories + legendCategories: legendCategories, // Always alphabetical for legend + categories: sectionCategories, // Custom order for sections + lastUsedTransforms: lastUsed, + showLastUsed: lastUsed.length > 0, + favorites: favorites, + showFavorites: favorites.length > 0 }; } + loadCategoryOrder() { + try { + const saved = localStorage.getItem('transformCategoryOrder'); + if (saved) { + return JSON.parse(saved); + } + } catch (e) { + console.warn('Failed to load category order:', e); + } + return null; + } + + mergeCategoryOrder(allCategories, savedOrder) { + // Always ensure randomizer is last + const categoriesWithoutRandomizer = allCategories.filter(c => c !== 'randomizer'); + + if (!savedOrder || savedOrder.length === 0) { + // Default: alphabetical, randomizer last + const sorted = categoriesWithoutRandomizer.sort((a, b) => a.localeCompare(b)); + return [...sorted, 'randomizer']; + } + + // Use saved order, but filter out categories that no longer exist and remove duplicates + const validSavedOrder = savedOrder + .filter(cat => allCategories.includes(cat)) + .filter((cat, index, arr) => arr.indexOf(cat) === index); // Remove duplicates + + // Find new categories not in saved order + const newCategories = categoriesWithoutRandomizer.filter(cat => !validSavedOrder.includes(cat)); + + // Build final order: saved order (filtered, deduplicated) + new categories (alphabetically) + randomizer + const finalOrder = [...validSavedOrder]; + if (newCategories.length > 0) { + finalOrder.push(...newCategories.sort((a, b) => a.localeCompare(b))); + } + + // Ensure randomizer is always last and remove any duplicates + const finalWithoutRandomizer = finalOrder.filter(c => c !== 'randomizer'); + const uniqueFinal = finalWithoutRandomizer.filter((cat, index, arr) => arr.indexOf(cat) === index); + return [...uniqueFinal, 'randomizer']; + } + + loadLastUsed() { + try { + const saved = localStorage.getItem('transformLastUsed'); + if (saved) { + const data = JSON.parse(saved); + // Filter to only include transforms that still exist + if (window.transforms) { + return data.filter(item => { + return Object.values(window.transforms).some(t => t.name === item.name); + }).slice(0, 5); // Keep only top 5 + } + } + } catch (e) { + console.warn('Failed to load last used transforms:', e); + } + return []; + } + + saveLastUsed(transformName) { + try { + let lastUsed = this.loadLastUsed(); + + // Remove if already exists + lastUsed = lastUsed.filter(item => item.name !== transformName); + + // Add to front with timestamp + lastUsed.unshift({ + name: transformName, + timestamp: Date.now() + }); + + // Keep only last 10 + lastUsed = lastUsed.slice(0, 10); + + localStorage.setItem('transformLastUsed', JSON.stringify(lastUsed)); + } catch (e) { + console.warn('Failed to save last used transform:', e); + } + } + + loadFavorites() { + try { + const saved = localStorage.getItem('transformFavorites'); + if (saved) { + const data = JSON.parse(saved); + // Filter to only include transforms that still exist + if (window.transforms) { + return data.filter(transformName => { + return Object.values(window.transforms).some(t => t.name === transformName); + }); + } + } + } catch (e) { + console.warn('Failed to load favorites:', e); + } + return []; + } + + saveFavorites(favorites) { + try { + localStorage.setItem('transformFavorites', JSON.stringify(favorites)); + } catch (e) { + console.warn('Failed to save favorites:', e); + } + } + getVueMethods() { return { getDisplayCategory: function(transformName) { @@ -70,6 +193,9 @@ class TransformTool extends Tool { if (this.transformInput) { this.activeTransform = transform; + // Track last used + this.saveLastUsedTransform(transform.name); + if (transform.name === 'Random Mix') { this.transformOutput = window.transforms.randomizer.func(this.transformInput); const transformInfo = window.transforms.randomizer.getLastTransformInfo(); @@ -103,6 +229,121 @@ class TransformTool extends Tool { this.ignoreKeyboardEvents = false; } }, + saveLastUsedTransform: function(transformName) { + try { + let lastUsed = this.lastUsedTransforms || []; + + // Remove if already exists + lastUsed = lastUsed.filter(item => item.name !== transformName); + + // Add to front with timestamp + lastUsed.unshift({ + name: transformName, + timestamp: Date.now() + }); + + // Keep only last 5 + lastUsed = lastUsed.slice(0, 5); + + this.lastUsedTransforms = lastUsed; + this.showLastUsed = lastUsed.length > 0; + + localStorage.setItem('transformLastUsed', JSON.stringify(lastUsed)); + } catch (e) { + console.warn('Failed to save last used transform:', e); + } + }, + getLastUsedTransforms: function() { + if (!this.lastUsedTransforms || this.lastUsedTransforms.length === 0) { + return []; + } + + return this.lastUsedTransforms + .map(item => { + return this.transforms.find(t => t.name === item.name); + }) + .filter(t => t !== undefined); + }, + toggleFavorite: function(transformName, event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const index = this.favorites.indexOf(transformName); + if (index > -1) { + // Remove from favorites + this.favorites.splice(index, 1); + this.showNotification('Removed from favorites', 'success', 'fas fa-star'); + } else { + // Add to favorites + this.favorites.push(transformName); + this.showNotification('Added to favorites', 'success', 'fas fa-star'); + } + + this.showFavorites = this.favorites.length > 0; + this.saveFavorites(this.favorites); + }, + isFavorite: function(transformName) { + return this.favorites && this.favorites.includes(transformName); + }, + getFavoriteTransforms: function() { + if (!this.favorites || this.favorites.length === 0) { + return []; + } + + return this.favorites + .map(transformName => { + return this.transforms.find(t => t.name === transformName); + }) + .filter(t => t !== undefined); + }, + saveFavorites: function(favorites) { + try { + localStorage.setItem('transformFavorites', JSON.stringify(favorites)); + } catch (e) { + console.warn('Failed to save favorites:', e); + } + }, + moveCategoryUp: function(categoryIndex) { + if (categoryIndex <= 0) return; + + // Never allow moving randomizer itself + if (this.categories[categoryIndex] === 'randomizer') return; + + // Use Vue's array mutation methods for proper reactivity + const categoryToMove = this.categories[categoryIndex]; + this.categories.splice(categoryIndex, 1); + this.categories.splice(categoryIndex - 1, 0, categoryToMove); + + this.saveCategoryOrder(this.categories); + this.showNotification('Category order saved', 'success', 'fas fa-check'); + }, + moveCategoryDown: function(categoryIndex) { + // Don't allow moving if already at or past the last valid position + // Last position is reserved for randomizer, so we can't move to it + if (categoryIndex >= this.categories.length - 2) return; + + // Never allow moving randomizer itself + if (this.categories[categoryIndex] === 'randomizer') return; + + // Use Vue's array mutation methods for proper reactivity + const categoryToMove = this.categories[categoryIndex]; + this.categories.splice(categoryIndex, 1); + this.categories.splice(categoryIndex + 1, 0, categoryToMove); + + this.saveCategoryOrder(this.categories); + this.showNotification('Category order saved', 'success', 'fas fa-check'); + }, + saveCategoryOrder: function(categories) { + try { + // Remove duplicates before saving + const uniqueCategories = categories.filter((cat, index, arr) => arr.indexOf(cat) === index); + localStorage.setItem('transformCategoryOrder', JSON.stringify(uniqueCategories)); + } catch (e) { + console.warn('Failed to save category order:', e); + } + }, autoTransform: function() { if (this.transformInput && this.activeTransform && this.activeTab === 'transforms') { const segments = window.EmojiUtils.splitEmojis(this.transformInput); @@ -171,6 +412,17 @@ class TransformTool extends Tool { return { mounted() { this.initializeCategoryNavigation(); + + // Save initial category order to localStorage if it doesn't exist + // This ensures consistent state for category reordering operations + try { + const saved = localStorage.getItem('transformCategoryOrder'); + if (!saved && this.categories && this.categories.length > 0) { + this.saveCategoryOrder(this.categories); + } + } catch (e) { + console.warn('Failed to check/save initial category order:', e); + } } }; } diff --git a/templates/splitter.html b/templates/splitter.html index a56d54c..1fbcff4 100644 --- a/templates/splitter.html +++ b/templates/splitter.html @@ -71,9 +71,25 @@
diff --git a/templates/transforms.html b/templates/transforms.html index e21a7f4..6ea17dd 100644 --- a/templates/transforms.html +++ b/templates/transforms.html @@ -14,7 +14,7 @@
Categories:
-