monorepo/native/desktop/maplefile/docs/COLLECTION_ICON_CUSTOMIZATION_PLAN.md

12 KiB

Collection Icon Customization Plan

Overview

Add the ability to customize collection (folder) icons with emojis or predefined icons. This feature enhances the user experience by allowing visual differentiation between collections.

Requirements

  1. Customization Options:

    • Custom emoji (e.g., 📁, 🎵, 📷, 💼, 🏠)
    • Predefined cross-browser icons from a curated set
    • Default folder icon when no customization is set
  2. Behavior:

    • Default to standard folder icon if no customization
    • Persist across browser sessions (stored in database)
    • Easy to change or revert to default
    • Intuitive, user-friendly UI
  3. Security Consideration:

    • Icon data should be encrypted (E2EE) like collection name
    • Only emoji characters or predefined icon identifiers allowed

Data Model Design

New Field: custom_icon

Add a new encrypted field to the Collection model that stores icon customization data.

// CustomIcon stores the collection's custom icon configuration
type CustomIcon struct {
    Type  string `json:"type"`  // "emoji", "icon", or "" (empty = default)
    Value string `json:"value"` // Emoji character or icon identifier
}

Field Storage Options:

Option Pros Cons
A) Single encrypted JSON field Simple, one field Requires parsing
B) Two fields (type + value) Clear structure More columns
C) Single string field Simplest Limited validation

Recommended: Option C - Single encrypted_custom_icon field storing either:

  • Empty string "" → Default folder icon
  • Emoji character (e.g., "📷") → Display as emoji
  • Icon identifier (e.g., "icon:briefcase") → Predefined icon

This keeps the schema simple and the client handles interpretation.


Implementation Plan

Phase 1: Backend (cloud/maplefile-backend)

1.1 Database Schema Update

Note: No new migration files needed - updating existing migration 012_create_collections_by_id.up.cql directly (assumes full database wipe).

Add to collections_by_id table:

encrypted_custom_icon TEXT,

1.2 Update Domain Model

File: internal/domain/collection/model.go

type Collection struct {
    // ... existing fields ...

    // EncryptedCustomIcon stores the custom icon for this collection.
    // Empty string means use default folder icon.
    // Contains either an emoji character or "icon:<identifier>" for predefined icons.
    // Encrypted with the collection key for E2EE.
    EncryptedCustomIcon string `bson:"encrypted_custom_icon" json:"encrypted_custom_icon"`
}

Also add to CollectionSyncItem for sync operations:

type CollectionSyncItem struct {
    // ... existing fields ...
    EncryptedCustomIcon string `json:"encrypted_custom_icon,omitempty" bson:"encrypted_custom_icon,omitempty"`
}

Note: The sync query in collectionsync.go:getCollectionSyncItem() fetches minimal data from collections_by_id. This query will need to include encrypted_custom_icon so clients can display the correct icon during sync.

1.3 Update Repository Layer

Files to modify:

  • internal/repo/collection/create.go - Include new field in INSERT
  • internal/repo/collection/update.go - Include new field in UPDATE
  • internal/repo/collection/get.go - Include new field in SELECT
  • internal/repo/collection/sync.go - Include new field in sync queries

1.4 Update HTTP Handlers (if needed)

The existing create/update endpoints should automatically handle the new field since they accept the full Collection struct.


Phase 2: Frontend (web/maplefile-frontend)

2.1 Create Icon Picker Component

New file: src/components/IconPicker/IconPicker.jsx

Features:

  • Emoji picker tab with common categories (objects, activities, symbols)
  • Predefined icons tab (Heroicons subset)
  • "Default" option to revert to folder icon
  • Search/filter functionality
  • Recently used icons
// Example structure
const IconPicker = ({ value, onChange, onClose }) => {
  const [activeTab, setActiveTab] = useState('emoji'); // 'emoji' | 'icons'

  const predefinedIcons = [
    { id: 'briefcase', icon: BriefcaseIcon, label: 'Work' },
    { id: 'photo', icon: PhotoIcon, label: 'Photos' },
    { id: 'music', icon: MusicalNoteIcon, label: 'Music' },
    { id: 'document', icon: DocumentIcon, label: 'Documents' },
    { id: 'archive', icon: ArchiveBoxIcon, label: 'Archive' },
    // ... more icons
  ];

  const popularEmojis = ['📁', '📷', '🎵', '💼', '🏠', '❤️', '⭐', '🎮', '📚', '🎨'];

  return (
    // ... picker UI
  );
};

2.2 Update CollectionEdit Page

File: src/pages/User/FileManager/Collections/CollectionEdit.jsx

Add icon customization section:

{/* Icon Customization Section */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
  <h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
    <SparklesIcon className="h-5 w-5 mr-2 text-gray-500" />
    Customize Icon
  </h2>

  <div className="flex items-center space-x-4">
    {/* Current Icon Preview */}
    <div className="flex items-center justify-center h-16 w-16 bg-gray-100 rounded-xl border-2 border-dashed border-gray-300">
      <CollectionIcon icon={formData.customIcon} size="lg" />
    </div>

    {/* Change/Reset Buttons */}
    <div className="space-y-2">
      <button onClick={() => setShowIconPicker(true)} className="...">
        Change Icon
      </button>
      {formData.customIcon && (
        <button onClick={() => setFormData({...formData, customIcon: ''})} className="...">
          Reset to Default
        </button>
      )}
    </div>
  </div>
</div>

2.3 Create CollectionIcon Component

New file: src/components/CollectionIcon/CollectionIcon.jsx

Renders the appropriate icon based on the customIcon value:

const CollectionIcon = ({ icon, collectionType = 'folder', size = 'md', className = '' }) => {
  const sizes = {
    sm: 'h-4 w-4',
    md: 'h-6 w-6',
    lg: 'h-10 w-10',
  };

  // Default folder/album icon
  if (!icon || icon === '') {
    const Icon = collectionType === 'album' ? PhotoIcon : FolderIcon;
    return <Icon className={`${sizes[size]} ${className}`} />;
  }

  // Predefined icon
  if (icon.startsWith('icon:')) {
    const iconId = icon.replace('icon:', '');
    const IconComponent = predefinedIconMap[iconId];
    return IconComponent ? <IconComponent className={`${sizes[size]} ${className}`} /> : <FolderIcon className={`${sizes[size]} ${className}`} />;
  }

  // Emoji
  return <span className={`${emojiSizes[size]} ${className}`}>{icon}</span>;
};

2.4 Update Collection List/Grid Views

Update anywhere collections are displayed to use the new CollectionIcon component:

  • FileManagerIndex.jsx
  • CollectionDetails.jsx
  • Sidebar navigation (if applicable)

2.5 Update Encryption/Decryption

Update the collection encryption service to handle the new field:

  • Encrypt customIcon when saving
  • Decrypt encrypted_custom_icon when loading

Phase 3: Native Desktop (native/desktop/maplefile)

3.1 Update Domain Model

File: internal/domain/collection/model.go

type Collection struct {
    // ... existing fields ...

    // CustomIcon stores the decrypted custom icon for this collection.
    // Empty string means use default folder icon.
    // Contains either an emoji character or "icon:<identifier>" for predefined icons.
    CustomIcon string `json:"custom_icon,omitempty"`

    // EncryptedCustomIcon is the encrypted version from cloud
    EncryptedCustomIcon string `json:"encrypted_custom_icon,omitempty"`
}

3.2 Update Sync Service

Ensure the sync service handles the new field when syncing collections from the cloud.

3.3 Update Frontend (Wails)

The desktop frontend uses the same React patterns, so the IconPicker and CollectionIcon components can be shared or adapted.


Predefined Icon Set

A curated set of cross-browser compatible icons:

ID Icon Use Case
briefcase BriefcaseIcon Work
photo PhotoIcon Photos
music MusicalNoteIcon Music
video VideoCameraIcon Videos
document DocumentTextIcon Documents
archive ArchiveBoxIcon Archive
star StarIcon Favorites
heart HeartIcon Personal
home HomeIcon Home
academic AcademicCapIcon School
code CodeBracketIcon Code
cloud CloudIcon Cloud
lock LockClosedIcon Private
gift GiftIcon Gifts
calendar CalendarIcon Events

Migration Strategy

  1. Backward Compatible: Empty encrypted_custom_icon means default icon
  2. No Data Migration Needed: New collections get the field, old collections have NULL/empty
  3. Clients Handle Missing Field: Treat NULL/empty as default

Testing Checklist

Backend

  • Migration runs successfully
  • Create collection with custom icon
  • Update collection custom icon
  • Revert to default icon
  • Sync includes custom icon field
  • E2EE encryption/decryption works

Frontend (Web)

  • Icon picker opens and closes
  • Emoji selection works
  • Predefined icon selection works
  • Reset to default works
  • Icon persists after page reload
  • Icon displays correctly in list/grid views
  • Works across different browsers

Native Desktop

  • Sync downloads custom icon
  • Icon displays correctly
  • Edit icon works (if implemented)

Security Considerations

  1. E2EE: Custom icon is encrypted with collection key
  2. Input Validation: Only allow valid emoji or predefined icon IDs
  3. XSS Prevention: Sanitize icon display (emoji rendering, no HTML)
  4. Size Limits: Max length for custom icon field (e.g., 50 chars)

Future Enhancements

  1. Custom uploaded icons (requires more complex storage)
  2. Icon color customization
  3. Icon packs/themes
  4. Bulk icon changes (apply to multiple collections)

Files to Modify

Backend (cloud/maplefile-backend)

Schema Update (modify existing migration):

  1. migrations/012_create_collections_by_id.up.cql - Add encrypted_custom_icon TEXT column

Domain Layer: 2. internal/domain/collection/model.go - Add EncryptedCustomIcon field to Collection and CollectionSyncItem structs

Repository Layer: 4. internal/repo/collection/create.go - Add encrypted_custom_icon to INSERT query (line ~57-66) 5. internal/repo/collection/update.go - Add encrypted_custom_icon to UPDATE query (line ~64-73) 6. internal/repo/collection/get.go - Add encrypted_custom_icon to SELECT query and scan (line ~44-52, ~72-90) 7. internal/repo/collection/collectionsync.go - Add field to sync queries

Note: Secondary tables (013-017) do NOT need modification - they are lookup/index tables that only store keys and minimal fields. The encrypted_custom_icon is stored only in collections_by_id.

Frontend (web/maplefile-frontend)

New Components:

  1. src/components/IconPicker/IconPicker.jsx - Modal with emoji grid + predefined icons
  2. src/components/CollectionIcon/CollectionIcon.jsx - Renders appropriate icon based on value

Modified Pages: 3. src/pages/User/FileManager/Collections/CollectionEdit.jsx - Add icon customization section 4. src/pages/User/FileManager/Collections/CollectionDetails.jsx - Display custom icon 5. src/pages/User/FileManager/Collections/CollectionCreate.jsx - Optional icon selection on create 6. src/pages/User/FileManager/FileManagerIndex.jsx - Display custom icons in list/grid

Services: 7. Collection encryption service - Handle customIcon field encryption/decryption

Native Desktop (native/desktop/maplefile)

Domain:

  1. internal/domain/collection/model.go - Add CustomIcon and EncryptedCustomIcon fields

Repository: 2. internal/repo/collection/repository.go - Handle new field in CRUD operations

Sync: 3. internal/service/sync/collection.go - Include field in sync operations

Frontend: 4. frontend/src/ - Adapt IconPicker and CollectionIcon components (if not shared with web)


Estimated Effort

Phase Effort
Backend (migration + model) 2-3 hours
Frontend components 4-6 hours
Frontend integration 2-3 hours
Native desktop 2-3 hours
Testing 2-3 hours
Total 12-18 hours