Implement mutually exclusive tagging with untagging, fix UI shifting, and add clapper emoji to title
This commit is contained in:
parent
2549295904
commit
e667dc37c9
163
PROJECT_OVERVIEW.md
Normal file
163
PROJECT_OVERVIEW.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# MovieMapper Project Overview
|
||||||
|
|
||||||
|
## Project Description
|
||||||
|
MovieMapper is a desktop application designed for organizing and managing movie and TV show collections. It provides a user-friendly interface for browsing media files, searching TV shows using TheTVDB API, and managing file metadata including durations and quality information.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Main Components
|
||||||
|
1. **Electron-based Desktop Application** - Cross-platform desktop application using Electron
|
||||||
|
2. **Main Process** (`main.js`) - Handles application lifecycle, IPC communication, and API interactions
|
||||||
|
3. **Renderer Process** (`renderer.js`) - Manages the user interface and user interactions
|
||||||
|
4. **File Utilities** (`utils/fileUtils.js`) - Handles file scanning, metadata extraction, and media processing
|
||||||
|
5. **Frontend Interface** (`index.html`) - HTML structure with CSS styling
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### 1. Directory Browsing and Media Scanning
|
||||||
|
- **Directory Selection**: Users can select any directory containing media files
|
||||||
|
- **Recursive Scanning**: Automatically scans subdirectories for media files
|
||||||
|
- **Media File Detection**: Identifies media files based on supported extensions (.mp4, .mkv, .avi, .mov, .flv, .webm)
|
||||||
|
- **File Metadata Extraction**:
|
||||||
|
- Video duration in mm:ss format
|
||||||
|
- Video quality (4K, 1080p, 720p, etc.)
|
||||||
|
- Frame rate information
|
||||||
|
- File size and modification date
|
||||||
|
|
||||||
|
### 2. TV Show Search and Management
|
||||||
|
- **TheTVDB API Integration**: Search and retrieve TV show information
|
||||||
|
- **Show Details**: View show information including:
|
||||||
|
- Series name
|
||||||
|
- Status
|
||||||
|
- First aired date
|
||||||
|
- Overview/plot summary
|
||||||
|
- Image/artwork
|
||||||
|
- **Season Management**: Browse seasons with episode counts
|
||||||
|
- **Episode Information**: View episode details including:
|
||||||
|
- Episode names
|
||||||
|
- Episode numbers
|
||||||
|
- Runtime information
|
||||||
|
- Air dates
|
||||||
|
|
||||||
|
### 3. File Management
|
||||||
|
- **File Renaming**: Click on file names to rename them directly in the interface
|
||||||
|
- **File Tagging**: Mark files as extras or with commentary tags
|
||||||
|
- **File Untagging**: Click on already tagged files to remove the tag
|
||||||
|
- **File Organization**: Move files to "extras" folder for better organization
|
||||||
|
- **Problematic File Handling**: Identifies and flags files that cause issues during metadata extraction
|
||||||
|
|
||||||
|
### 4. Technical Features
|
||||||
|
- **Electron Framework**: Cross-platform desktop application
|
||||||
|
- **FFmpeg Integration**: For video metadata extraction (duration, quality, frame rate)
|
||||||
|
- **API Error Handling**: Robust error handling for API calls and file operations
|
||||||
|
- **IPC Communication**: Inter-process communication between main and renderer processes
|
||||||
|
- **Environment Configuration**: API keys and configuration loaded from .env file
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Main Process (`main.js`)
|
||||||
|
- **IPC Handlers**:
|
||||||
|
- Directory selection and scanning
|
||||||
|
- File renaming
|
||||||
|
- TVDB API integration (search, show details, season episodes)
|
||||||
|
- File system operations (moving files to extras)
|
||||||
|
- Debugging utilities
|
||||||
|
- **Electron Integration**: Window management, application lifecycle
|
||||||
|
- **API Authentication**: Handles TVDB v4 API authentication with bearer tokens
|
||||||
|
|
||||||
|
### Renderer Process (`renderer.js`)
|
||||||
|
- **UI Management**: Handles all user interface interactions
|
||||||
|
- **Event Handling**: Click events, input handling, form submissions
|
||||||
|
- **File Display**: Renders file lists with metadata
|
||||||
|
- **Search Functionality**: Debounced search with API integration
|
||||||
|
- **Editing Support**: Makes file names editable with save functionality
|
||||||
|
- **Tagging System**: Visual indicators for tagged files
|
||||||
|
|
||||||
|
### File Utilities (`utils/fileUtils.js`)
|
||||||
|
- **Directory Scanning**: Recursively scans directories for media files
|
||||||
|
- **Metadata Extraction**:
|
||||||
|
- `extractFileDuration()`: Gets video duration using FFmpeg
|
||||||
|
- `extractVideoQuality()`: Determines video quality and frame rate
|
||||||
|
- `extractFileMetadata()`: Comprehensive metadata extraction
|
||||||
|
- **Error Handling**: Graceful handling of permission errors and file issues
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
|
||||||
|
### TheTVDB v4 API
|
||||||
|
- **Authentication**: Uses bearer token authentication
|
||||||
|
- **Endpoints**:
|
||||||
|
- Search: `https://api4.thetvdb.com/v4/search`
|
||||||
|
- Show Details: `https://api4.thetvdb.com/v4/series/{id}/extended`
|
||||||
|
- Episodes: `https://api4.thetvdb.com/v4/series/{id}/episodes`
|
||||||
|
- **Configuration**: API key loaded from environment variables
|
||||||
|
|
||||||
|
## User Interface
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
- **Sidebar**:
|
||||||
|
- Directory selection controls
|
||||||
|
- TV show search input
|
||||||
|
- Search results display
|
||||||
|
- Show details and seasons display
|
||||||
|
- **Main Content Area**:
|
||||||
|
- File listing with metadata
|
||||||
|
- File renaming capability
|
||||||
|
- File tagging indicators
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Responsive Design**: Clean, organized interface
|
||||||
|
- **Visual Feedback**:
|
||||||
|
- File renaming with editing support
|
||||||
|
- Tagging indicators with visual effects
|
||||||
|
- Problematic file highlighting
|
||||||
|
- **Search Integration**: Real-time search with debouncing
|
||||||
|
- **Episode Display**: Organized season and episode information
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
- **Electron**: Desktop application framework
|
||||||
|
- **fluent-ffmpeg**: Video metadata extraction
|
||||||
|
- **axios**: HTTP client for API requests
|
||||||
|
- **dotenv**: Environment variable management
|
||||||
|
|
||||||
|
### Development Dependencies
|
||||||
|
- **package.json**: Project metadata and dependencies
|
||||||
|
- **package-lock.json**: Exact dependency versions
|
||||||
|
|
||||||
|
## Installation and Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
1. Node.js installed
|
||||||
|
2. FFmpeg installed (required for video metadata extraction)
|
||||||
|
3. TheTVDB API key (included in .env file)
|
||||||
|
|
||||||
|
### Setup Process
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install dependencies: `npm install`
|
||||||
|
3. Run the application: `npm start` or `electron .`
|
||||||
|
|
||||||
|
## Usage Workflow
|
||||||
|
|
||||||
|
1. **Select Directory**: Click "Select Directory" to choose a folder containing media files
|
||||||
|
2. **Scan Files**: Application automatically scans the directory and displays media files with metadata
|
||||||
|
3. **Browse Files**: View files with duration, quality, and frame rate information
|
||||||
|
4. **Rename Files**: Click on any file name to rename it
|
||||||
|
5. **Search Shows**: Use the search functionality to find TV shows and their details
|
||||||
|
6. **View Seasons**: Click on seasons to see episode information
|
||||||
|
7. **Organize Files**: Use tagging and moving to extras functionality for better organization
|
||||||
|
|
||||||
|
## Error Handling and Debugging
|
||||||
|
|
||||||
|
- **File System Errors**: Graceful handling of permission errors and missing files
|
||||||
|
- **API Errors**: Comprehensive error handling for TVDB API calls
|
||||||
|
- **Metadata Extraction**: Fallbacks when FFmpeg fails to extract information
|
||||||
|
- **Debug Tools**: Built-in debugging utilities for problematic files
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- Enhanced file matching algorithms for better episode identification
|
||||||
|
- Database storage for persistent show and file information
|
||||||
|
- Additional tagging and categorization features
|
||||||
|
- Export functionality for organized collections
|
||||||
|
- Advanced search and filtering options
|
||||||
28
index.html
28
index.html
@ -166,7 +166,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<h1>Movie Mapper</h1>
|
<h1>Movie Mapper 🎬</h1>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button id="select-dir-btn">Select Directory</button>
|
<button id="select-dir-btn">Select Directory</button>
|
||||||
@ -269,6 +269,7 @@
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-icon:hover {
|
.tag-icon:hover {
|
||||||
@ -276,6 +277,31 @@
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* For tagged state, use pseudo-elements to show the highlight */
|
||||||
|
.tag-icon[data-tagged-extra]::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-icon[data-tagged-commentary]::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(25, 160, 184, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
.extra-tag {
|
.extra-tag {
|
||||||
color: #ffc107;
|
color: #ffc107;
|
||||||
}
|
}
|
||||||
|
|||||||
229
renderer.js
229
renderer.js
@ -86,6 +86,10 @@ function displayFiles(files) {
|
|||||||
<div class="file-duration">${duration}</div>
|
<div class="file-duration">${duration}</div>
|
||||||
<div class="file-quality">${quality}</div>
|
<div class="file-quality">${quality}</div>
|
||||||
<div class="file-fps">${fps}</div>
|
<div class="file-fps">${fps}</div>
|
||||||
|
<div class="file-tags">
|
||||||
|
<span class="tag-icon extra-tag" data-file-path="${file.path}" title="Mark as Extra">🏷️</span>
|
||||||
|
<span class="tag-icon commentary-tag" data-file-path="${file.path}" title="Add Commentary">💬</span>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add click event to make file name editable
|
// Add click event to make file name editable
|
||||||
@ -94,11 +98,42 @@ function displayFiles(files) {
|
|||||||
makeEditable(e.target);
|
makeEditable(e.target);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add context menu event listener to the file item itself
|
// Add hover effects for tags
|
||||||
fileItem.addEventListener('contextmenu', function(e) {
|
const tagIcons = fileItem.querySelectorAll('.tag-icon');
|
||||||
console.log('File item contextmenu event triggered');
|
tagIcons.forEach(icon => {
|
||||||
e.preventDefault();
|
icon.addEventListener('mouseenter', function() {
|
||||||
// This will be handled by the global listener
|
this.style.opacity = '1';
|
||||||
|
this.style.transform = 'scale(1.1)';
|
||||||
|
});
|
||||||
|
|
||||||
|
icon.addEventListener('mouseleave', function() {
|
||||||
|
this.style.opacity = '0.7';
|
||||||
|
this.style.transform = 'scale(1)';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add click handlers for tagging
|
||||||
|
icon.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation(); // Prevent event bubbling
|
||||||
|
const filePath = this.dataset.filePath;
|
||||||
|
const tagType = this.classList.contains('extra-tag') ? 'extra' : 'commentary';
|
||||||
|
|
||||||
|
// Check if file is already tagged with this type
|
||||||
|
const fileItem = this.closest('.file-item');
|
||||||
|
const isTagged = fileItem.hasAttribute('data-tagged-' + tagType);
|
||||||
|
|
||||||
|
if (isTagged) {
|
||||||
|
// Untag the file
|
||||||
|
untagFile(filePath, tagType);
|
||||||
|
} else {
|
||||||
|
// If tagging this type, untag any existing tag of the other type
|
||||||
|
const otherTagType = tagType === 'extra' ? 'commentary' : 'extra';
|
||||||
|
if (fileItem.hasAttribute('data-tagged-' + otherTagType)) {
|
||||||
|
untagFile(filePath, otherTagType);
|
||||||
|
}
|
||||||
|
// Tag the file
|
||||||
|
addTagToEpisode(filePath, tagType);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
fileListEl.appendChild(fileItem);
|
fileListEl.appendChild(fileItem);
|
||||||
@ -167,105 +202,67 @@ function makeEditable(element) {
|
|||||||
element.addEventListener('blur', saveEdit);
|
element.addEventListener('blur', saveEdit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add context menu functionality for files
|
// Function to handle tagging
|
||||||
function addFileContextMenu() {
|
function addTagToEpisode(filePath, tagType) {
|
||||||
// Add context menu listener to file items
|
console.log(`Tagging file ${filePath} as ${tagType}`);
|
||||||
document.addEventListener('contextmenu', function(e) {
|
|
||||||
console.log('Context menu event fired on:', e.target);
|
|
||||||
console.log('Target class list:', e.target.classList);
|
|
||||||
|
|
||||||
// Check if the right-click was on a file name
|
// Find the file item in the UI
|
||||||
if (e.target.classList.contains('file-name')) {
|
const fileItems = document.querySelectorAll('.file-item');
|
||||||
console.log('Right-click detected on file name');
|
fileItems.forEach(item => {
|
||||||
e.preventDefault();
|
const fileNameElement = item.querySelector('.file-name');
|
||||||
|
if (fileNameElement && fileNameElement.dataset.filePath === filePath) {
|
||||||
|
// Add visual indication of tagging using data attribute for CSS styling
|
||||||
|
const tagIcon = item.querySelector(`.${tagType}-tag`);
|
||||||
|
if (tagIcon) {
|
||||||
|
// Make the icon fully saturated and highlight in bright yellow with very strong visual effect
|
||||||
|
tagIcon.style.opacity = '1';
|
||||||
|
tagIcon.style.filter = 'none';
|
||||||
|
tagIcon.style.color = tagType === 'extra' ? '#FFD700' : '#17a2b8'; // Brighter yellow or teal color
|
||||||
|
tagIcon.style.textShadow = '0 0 15px rgba(255, 215, 0, 1)'; // Very bright yellow glow (for extra tags)
|
||||||
|
tagIcon.style.transform = 'scale(1.3)'; // More pronounced enlargement
|
||||||
|
tagIcon.style.boxShadow = '0 0 10px rgba(255, 215, 0, 0.8)'; // Additional glow effect
|
||||||
|
|
||||||
const filePath = e.target.dataset.filePath;
|
// Add a data attribute to track that this file is tagged
|
||||||
const fileName = e.target.textContent.trim();
|
item.setAttribute('data-tagged-' + tagType, 'true');
|
||||||
|
|
||||||
// Create context menu
|
|
||||||
const contextMenu = document.createElement('div');
|
|
||||||
contextMenu.id = 'file-context-menu';
|
|
||||||
contextMenu.style.position = 'absolute';
|
|
||||||
contextMenu.style.left = e.pageX + 'px';
|
|
||||||
contextMenu.style.top = e.pageY + 'px';
|
|
||||||
contextMenu.style.backgroundColor = 'white';
|
|
||||||
contextMenu.style.border = '1px solid #ddd';
|
|
||||||
contextMenu.style.borderRadius = '4px';
|
|
||||||
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
|
|
||||||
contextMenu.style.zIndex = '1000';
|
|
||||||
contextMenu.style.padding = '5px 0';
|
|
||||||
contextMenu.style.minWidth = '150px';
|
|
||||||
|
|
||||||
// Add "Move to Extras" option
|
|
||||||
const moveToExtras = document.createElement('div');
|
|
||||||
moveToExtras.textContent = 'Move to Extras';
|
|
||||||
moveToExtras.style.padding = '8px 16px';
|
|
||||||
moveToExtras.style.cursor = 'pointer';
|
|
||||||
moveToExtras.style.fontFamily = 'Arial, sans-serif';
|
|
||||||
moveToExtras.style.fontSize = '14px';
|
|
||||||
|
|
||||||
moveToExtras.addEventListener('mouseenter', function() {
|
|
||||||
this.style.backgroundColor = '#f0f0f0';
|
|
||||||
});
|
|
||||||
|
|
||||||
moveToExtras.addEventListener('mouseleave', function() {
|
|
||||||
this.style.backgroundColor = 'transparent';
|
|
||||||
});
|
|
||||||
|
|
||||||
moveToExtras.addEventListener('click', async function() {
|
|
||||||
try {
|
|
||||||
console.log('Attempting to move file to extras:', filePath);
|
|
||||||
const result = await ipcRenderer.invoke('move-to-extras', filePath);
|
|
||||||
if (result.success) {
|
|
||||||
console.log('File moved successfully');
|
|
||||||
// Remove the file from the UI
|
|
||||||
const fileItem = e.target.closest('.file-item');
|
|
||||||
if (fileItem) {
|
|
||||||
fileItem.remove();
|
|
||||||
}
|
|
||||||
alert(`File moved to extras: ${fileName}`);
|
|
||||||
} else {
|
|
||||||
console.error('Failed to move file:', result.error);
|
|
||||||
alert('Failed to move file to extras: ' + result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error moving file:', error);
|
|
||||||
alert('Error moving file to extras: ' + error.message);
|
|
||||||
}
|
|
||||||
// Remove context menu
|
|
||||||
document.getElementById('file-context-menu')?.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
contextMenu.appendChild(moveToExtras);
|
|
||||||
|
|
||||||
// Add click outside to close menu
|
|
||||||
function closeMenu(e) {
|
|
||||||
if (!contextMenu.contains(e.target)) {
|
|
||||||
document.removeEventListener('click', closeMenu);
|
|
||||||
contextMenu.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('click', closeMenu);
|
|
||||||
|
|
||||||
document.body.appendChild(contextMenu);
|
|
||||||
|
|
||||||
// Also add a click outside listener to the document
|
|
||||||
document.addEventListener('mousedown', function closeOnOutsideClick(e) {
|
|
||||||
if (!contextMenu.contains(e.target)) {
|
|
||||||
document.removeEventListener('mousedown', closeOnOutsideClick);
|
|
||||||
contextMenu.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// In a real implementation, this would save to a database or file
|
||||||
|
// For now, we'll just log to console
|
||||||
|
console.log(`File ${filePath} tagged as ${tagType} - would be saved in real implementation`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize context menu when the app loads
|
// Function to handle untagging
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
function untagFile(filePath, tagType) {
|
||||||
console.log('DOM loaded - adding context menu');
|
console.log(`Untagging file ${filePath} from ${tagType}`);
|
||||||
addFileContextMenu();
|
|
||||||
});
|
// Find the file item in the UI
|
||||||
|
const fileItems = document.querySelectorAll('.file-item');
|
||||||
|
fileItems.forEach(item => {
|
||||||
|
const fileNameElement = item.querySelector('.file-name');
|
||||||
|
if (fileNameElement && fileNameElement.dataset.filePath === filePath) {
|
||||||
|
// Remove visual indication of tagging
|
||||||
|
const tagIcon = item.querySelector(`.${tagType}-tag`);
|
||||||
|
if (tagIcon) {
|
||||||
|
// Reset to original appearance
|
||||||
|
tagIcon.style.opacity = '0.7';
|
||||||
|
tagIcon.style.filter = 'none';
|
||||||
|
tagIcon.style.color = ''; // Reset to default color
|
||||||
|
tagIcon.style.textShadow = 'none';
|
||||||
|
tagIcon.style.transform = 'scale(1)';
|
||||||
|
tagIcon.style.boxShadow = 'none';
|
||||||
|
|
||||||
|
// Remove the data attribute
|
||||||
|
item.removeAttribute('data-tagged-' + tagType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// In a real implementation, this would remove from database or file
|
||||||
|
// For now, we'll just log to console
|
||||||
|
console.log(`File ${filePath} untagged from ${tagType} - would be removed in real implementation`);
|
||||||
|
}
|
||||||
|
|
||||||
// Search shows function
|
// Search shows function
|
||||||
async function searchShows() {
|
async function searchShows() {
|
||||||
@ -614,44 +611,6 @@ function updateFileListWithEpisodeInfo(episodes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced file display that can show episode matching info
|
|
||||||
function displayFiles(files) {
|
|
||||||
fileListEl.innerHTML = '';
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
fileListEl.innerHTML = '<p>No media files found in this directory.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
files.forEach(file => {
|
|
||||||
const fileItem = document.createElement('div');
|
|
||||||
fileItem.className = 'file-item';
|
|
||||||
|
|
||||||
// Use actual duration from file metadata
|
|
||||||
const duration = file.duration || '00:00';
|
|
||||||
const quality = file.quality || 'unknown';
|
|
||||||
const fps = file.fps || 'unknown';
|
|
||||||
|
|
||||||
// Add label for problematic files
|
|
||||||
const problemLabel = file.isProblematic ? '<span class="problematic-label">⚠️</span> ' : '';
|
|
||||||
|
|
||||||
fileItem.innerHTML = `
|
|
||||||
<div class="file-name" data-file-path="${file.path}">${problemLabel}${file.name}</div>
|
|
||||||
<div class="file-duration">${duration}</div>
|
|
||||||
<div class="file-quality">${quality}</div>
|
|
||||||
<div class="file-fps">${fps}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add click event to make file name editable
|
|
||||||
const fileNameElement = fileItem.querySelector('.file-name');
|
|
||||||
fileNameElement.addEventListener('click', function(e) {
|
|
||||||
makeEditable(e.target);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileListEl.appendChild(fileItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce function for search input
|
// Debounce function for search input
|
||||||
function debounce(func, wait) {
|
function debounce(func, wait) {
|
||||||
let timeout;
|
let timeout;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user