Fix arrow button episode number cascading - all subsequent episodes now shift correctly
This commit is contained in:
parent
30eba3ae4e
commit
46bfba582d
149
AGENTS.md
149
AGENTS.md
@ -8,31 +8,37 @@ MovieMapper is an Electron-based desktop application for organizing and managing
|
|||||||
### Build Commands
|
### Build Commands
|
||||||
- `npm install` - Install project dependencies
|
- `npm install` - Install project dependencies
|
||||||
- `npm start` - Run the application using Electron
|
- `npm start` - Run the application using Electron
|
||||||
- `electron .` - Alternative way to run the application
|
|
||||||
|
|
||||||
### Test Commands
|
### Test Commands
|
||||||
- `npm test` - Currently outputs "Error: no test specified" (default test script)
|
- `node test-api.js` - Test TheTVDB API connectivity
|
||||||
- `node test-api.js` - Run API connectivity test for TheTVDB
|
- `node test-command-line.js` - Test command-line argument parsing
|
||||||
- `node main.js` - Run main process directly for debugging (may require environment setup)
|
- `node test-audit.js` - Run unit tests for audit logging (uses `node:test`)
|
||||||
|
- `node test-functional.js` - Run functional tests for UI elements
|
||||||
|
- `node test-core.js` - Verify core functionality implementation
|
||||||
|
- `node test-implementation.js` - Test implemented features
|
||||||
|
- `node test-audit-functionality.js` - Test audit file operations
|
||||||
|
|
||||||
### Syntax Checking
|
### Syntax Checking
|
||||||
- `node --check renderer.js` - Check renderer.js for syntax errors
|
- `node --check renderer.js` - Check renderer.js for syntax errors
|
||||||
- `node --check main.js` - Check main.js for syntax errors
|
- `node --check main.js` - Check main.js for syntax errors
|
||||||
- `node --check utils/fileUtils.js` - Check fileUtils.js for syntax errors
|
- `node --check utils/fileUtils.js` - Check fileUtils.js for syntax errors
|
||||||
|
|
||||||
|
### Running Single Tests
|
||||||
|
Use `node <test-file>.js` to run individual test files. Tests use Node.js built-in `node:test` framework.
|
||||||
|
|
||||||
### Linting
|
### Linting
|
||||||
- No explicit linting tools configured in package.json
|
- No explicit linting tools configured
|
||||||
- Code style follows JavaScript/Node.js conventions
|
- Code style follows JavaScript/Node.js conventions
|
||||||
- Use JSDoc comments for documentation
|
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
### Main Process (`main.js`)
|
### Main Process (`main.js`)
|
||||||
- Handles all IPC communication with renderer
|
- Electron main process handling application lifecycle
|
||||||
- File system operations (moving, renaming files)
|
- IPC handlers for renderer communication (`ipcMain.handle()`)
|
||||||
|
- File system operations (moving, renaming, scanning)
|
||||||
- TheTVDB API integration with token authentication
|
- TheTVDB API integration with token authentication
|
||||||
- Audit logging to `.audit` files in directories
|
- Audit logging to `.audit` files in directories
|
||||||
- Progress reporting via IPC events
|
- Progress reporting via `mainWindow.webContents.send()`
|
||||||
|
|
||||||
### Renderer Process (`renderer.js`)
|
### Renderer Process (`renderer.js`)
|
||||||
- UI state management and DOM manipulation
|
- UI state management and DOM manipulation
|
||||||
@ -50,43 +56,41 @@ MovieMapper is an Electron-based desktop application for organizing and managing
|
|||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
### Imports
|
### Imports
|
||||||
- Use standard Node.js `require()` syntax for modules
|
- Use standard Node.js `require()` syntax
|
||||||
- Import modules at the top of files
|
- Import modules at the top of files
|
||||||
- Group imports by standard library, external modules, then local modules
|
- Group imports: standard library → external modules → local modules
|
||||||
- Use descriptive names for modules (e.g., `const fs = require('fs')`)
|
- Use descriptive names: `const fs = require('fs')`
|
||||||
|
|
||||||
### Formatting
|
### Formatting
|
||||||
- Use 2-space indentation
|
- 2-space indentation
|
||||||
- Use single quotes for strings
|
- Single quotes for strings
|
||||||
- Place opening braces on the same line as the statement
|
- Opening braces on same line as statement
|
||||||
- Add spaces around operators and after commas
|
- Spaces around operators and after commas
|
||||||
- No semicolons required (follows JavaScript convention)
|
- No semicolons required
|
||||||
- Use consistent spacing around code blocks
|
- Consistent spacing around code blocks
|
||||||
|
|
||||||
### Naming Conventions
|
### Naming Conventions
|
||||||
- Use camelCase for variables and functions
|
- camelCase for variables and functions
|
||||||
- Use PascalCase for constructors
|
- PascalCase for constructors
|
||||||
- Use UPPER_CASE for constants
|
- UPPER_CASE for constants
|
||||||
- Use descriptive variable names
|
|
||||||
- Function names should be verbs (e.g., `scanDirectory`, `extractFileMetadata`)
|
- Function names should be verbs (e.g., `scanDirectory`, `extractFileMetadata`)
|
||||||
- File names should be lowercase with hyphens (e.g., `file-utils.js`)
|
- File names lowercase with hyphens (e.g., `file-utils.js`)
|
||||||
|
|
||||||
### Types
|
### Types
|
||||||
- This is a JavaScript project without TypeScript
|
- JavaScript project without TypeScript
|
||||||
- Use JSDoc comments to document function parameters and return values
|
- Use JSDoc comments for function parameters and return values
|
||||||
- Use descriptive parameter names
|
|
||||||
- Include type information in comments for complex objects
|
- Include type information in comments for complex objects
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
- Use try/catch blocks for async operations
|
- Use try/catch blocks for async operations
|
||||||
- Handle file system errors gracefully
|
- Handle file system errors gracefully
|
||||||
- Log warnings for problematic files but don't crash the application
|
- Log warnings for problematic files without crashing
|
||||||
- Return consistent error structures: `{ success: true/false, error?: string, ... }`
|
- Return consistent error structures: `{ success: true/false, error?: string, ... }`
|
||||||
- Use specific error messages with context information
|
- Use specific error messages with context information
|
||||||
- Implement proper error propagation throughout the application
|
- Implement proper error propagation throughout the application
|
||||||
|
|
||||||
### IPC Communication Patterns
|
### IPC Communication Patterns
|
||||||
- Main process handlers use `ipcMain.handle()` for async operations
|
- Main process uses `ipcMain.handle()` for async operations
|
||||||
- Renderer invokes with `ipcRenderer.invoke()` returning promises
|
- Renderer invokes with `ipcRenderer.invoke()` returning promises
|
||||||
- Progress updates use `mainWindow.webContents.send()` for push events
|
- Progress updates use `mainWindow.webContents.send()` for push events
|
||||||
- Renderer listens with `ipcRenderer.on()` for progress events
|
- Renderer listens with `ipcRenderer.on()` for progress events
|
||||||
@ -108,13 +112,13 @@ MovieMapper is an Electron-based desktop application for organizing and managing
|
|||||||
- Implement authentication token caching for performance
|
- Implement authentication token caching for performance
|
||||||
|
|
||||||
### Code Patterns
|
### Code Patterns
|
||||||
- Use async/await for handling asynchronous operations
|
- Use async/await for asynchronous operations
|
||||||
- Handle permission errors gracefully when scanning directories
|
- Handle permission errors gracefully when scanning directories
|
||||||
- Provide user feedback through console warnings for problematic files
|
- Provide user feedback through console warnings for problematic files
|
||||||
- Use descriptive variable names that reflect their purpose
|
- Use descriptive variable names that reflect their purpose
|
||||||
- Implement comprehensive logging for debugging
|
- Implement comprehensive logging for debugging
|
||||||
- Use consistent IPC patterns for communication between main and renderer processes
|
- Use consistent IPC patterns for main/renderer communication
|
||||||
- Use `setImmediate` yields when sending IPC messages in loops to allow event loop processing
|
- Use `setImmediate` yields when sending IPC messages in loops
|
||||||
|
|
||||||
### Debugging
|
### Debugging
|
||||||
- Built-in debugging utilities for problematic files
|
- Built-in debugging utilities for problematic files
|
||||||
@ -125,17 +129,17 @@ MovieMapper is an Electron-based desktop application for organizing and managing
|
|||||||
- Audit logs written to `.audit` files in each directory
|
- Audit logs written to `.audit` files in each directory
|
||||||
|
|
||||||
### Environment Configuration
|
### Environment Configuration
|
||||||
- API keys stored in `.env` file
|
- API keys stored in `.env` file (root level only)
|
||||||
- Environment variables should be validated on application startup
|
- Environment variables validated on application startup
|
||||||
- Provide clear error messages when environment variables are missing
|
- Clear error messages when environment variables missing
|
||||||
- Use `dotenv` package for loading environment variables
|
- Use `dotenv` package for loading environment variables
|
||||||
|
|
||||||
## Key Features Implementation
|
## Key Features Implementation
|
||||||
|
|
||||||
### File Tagging System
|
### File Tagging System
|
||||||
- Files can be tagged as "extra" or "commentary"
|
- Files tagged as "extra" or "commentary"
|
||||||
- Tags stored as data attributes on DOM elements: `data-tagged-extra`, `data-tagged-commentary`
|
- Tags stored as data attributes: `data-tagged-extra`, `data-tagged-commentary`
|
||||||
- Visual feedback with color changes and glow effects when tagged
|
- Visual feedback with color changes and glow effects
|
||||||
- Play button enables only when file is tagged
|
- Play button enables only when file is tagged
|
||||||
- Individual play button moves single file
|
- Individual play button moves single file
|
||||||
- Floating action button (FAB) moves all tagged files at once
|
- Floating action button (FAB) moves all tagged files at once
|
||||||
@ -159,58 +163,6 @@ MovieMapper is an Electron-based desktop application for organizing and managing
|
|||||||
- Audit log entry written for each move operation
|
- Audit log entry written for each move operation
|
||||||
- Files removed from UI after successful move
|
- Files removed from UI after successful move
|
||||||
|
|
||||||
## Recent Changes (Session 2026-02-22)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
1. **Fixed duplicate `moveTaggedFile` function** - Two definitions were overwriting each other
|
|
||||||
2. **Fixed missing click handler for FAB** - Added `moveAllTaggedFiles()` on floating circle click
|
|
||||||
3. **Fixed "extra" vs "extras" folder naming** - Now correctly uses "extras" for Jellyfin
|
|
||||||
4. **Fixed progressBar reference error** - Removed reference after switching to spinner
|
|
||||||
5. **Removed non-existent test-api-btn reference** - Was causing errors on load
|
|
||||||
|
|
||||||
### New Features
|
|
||||||
1. **Folder navigation** - Click folders to navigate into them
|
|
||||||
2. **Non-recursive scanning** - Only shows current directory contents
|
|
||||||
3. **Progress spinner** - Shows file being processed with count
|
|
||||||
4. **Batch file moving** - FAB moves all tagged files at once
|
|
||||||
5. **Visual feedback** - FAB turns yellow while processing
|
|
||||||
|
|
||||||
### UI Improvements
|
|
||||||
1. **Dark theme** - Modern dark color scheme (#1a1a2e, #16213e, #0f3460)
|
|
||||||
2. **Clean layout** - Search/shows on left sidebar, files on right
|
|
||||||
3. **Accent color** - Pink/red accent (#e94560)
|
|
||||||
4. **Better typography** - System font stack
|
|
||||||
5. **Smooth animations** - Hover effects and transitions
|
|
||||||
|
|
||||||
## Jellyfin Compatible Folder Names
|
|
||||||
|
|
||||||
### Extras Folders (place inside movie/show folder)
|
|
||||||
- `behind the scenes` - Behind-the-scenes content
|
|
||||||
- `deleted scenes` - Deleted scenes
|
|
||||||
- `interviews` - Cast/crew interviews
|
|
||||||
- `scenes` - Individual scenes
|
|
||||||
- `samples` - Sample clips
|
|
||||||
- `shorts` - Short films
|
|
||||||
- `featurettes` - Featurettes
|
|
||||||
- `clips` - Clips
|
|
||||||
- `other` - Generic catch-all for unknown extras
|
|
||||||
- `extras` - Generic catch-all for unknown extras (CURRENTLY IMPLEMENTED)
|
|
||||||
- `trailers` - Trailers and previews
|
|
||||||
- `theme-music` - Theme music audio files
|
|
||||||
- `backdrops` - Backdrop videos
|
|
||||||
|
|
||||||
### Special Single-File Names (in same folder as media)
|
|
||||||
- `trailer` - Single trailer file
|
|
||||||
- `sample` - Single sample file
|
|
||||||
- `theme` - Theme song audio file
|
|
||||||
|
|
||||||
### File Suffix Options (append to filename)
|
|
||||||
- `-trailer`, `.trailer`, `_trailer`, ` trailer`
|
|
||||||
- `-sample`, `.sample`, `_sample`, ` sample`
|
|
||||||
- `-scene`, `-clip`, `-interview`
|
|
||||||
- `-behindthescenes`, `-deleted`, `-deletedscene`
|
|
||||||
- `-featurette`, `-short`, `-other`, `-extra`
|
|
||||||
|
|
||||||
## Known Issues & Future Work
|
## Known Issues & Future Work
|
||||||
|
|
||||||
### Known Issues
|
### Known Issues
|
||||||
@ -228,6 +180,21 @@ MovieMapper is an Electron-based desktop application for organizing and managing
|
|||||||
7. Add confirmation dialog before moving files
|
7. Add confirmation dialog before moving files
|
||||||
|
|
||||||
## Execution Constraints
|
## Execution Constraints
|
||||||
- All commands that might block indefinitely must be run with a timeout
|
- All commands that might block indefinitely must run with timeout
|
||||||
- Example: `timeout 30 npm start` to run the application with 30-second timeout
|
- Example: `timeout 30 npm start` to run with 30-second timeout
|
||||||
- Always run syntax checks after modifying JS files: `node --check <file>`
|
- Always run syntax checks after modifying JS files: `node --check <file>`
|
||||||
|
|
||||||
|
## Jellyfin Compatible Folder Names
|
||||||
|
|
||||||
|
### Extras Folders (place inside movie/show folder)
|
||||||
|
- `behind the scenes`, `deleted scenes`, `interviews`, `scenes`, `samples`, `shorts`, `featurettes`, `clips`, `other`, `extras`, `trailers`, `theme-music`, `backdrops`
|
||||||
|
|
||||||
|
### Special Single-File Names (in same folder as media)
|
||||||
|
- `trailer`, `sample`, `theme`
|
||||||
|
|
||||||
|
### File Suffix Options (append to filename)
|
||||||
|
- `-trailer`, `.trailer`, `_trailer`, ` trailer`
|
||||||
|
- `-sample`, `.sample`, `_sample`, ` sample`
|
||||||
|
- `-scene`, `-clip`, `-interview`
|
||||||
|
- `-behindthescenes`, `-deleted`, `-deletedscene`
|
||||||
|
- `-featurette`, `-short`, `-other`, `-extra`
|
||||||
43
index.html
43
index.html
@ -280,6 +280,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.episode-number-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.episode-number {
|
.episode-number {
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@ -291,7 +298,36 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-right: 12px;
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-arrow {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #0f3460;
|
||||||
|
color: #eee;
|
||||||
|
font-size: 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-arrow:hover {
|
||||||
|
background-color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-arrow:active {
|
||||||
|
background-color: #16213e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-arrow:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
@ -487,7 +523,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="begin-mapping-btn" class="begin-mapping-btn">Begin Mapping</button>
|
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
||||||
|
<button id="update-ranges-btn" class="begin-mapping-btn" style="flex: 1;">Update Ranges</button>
|
||||||
|
<button id="begin-mapping-btn" class="begin-mapping-btn" style="flex: 1;">Begin Mapping</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
40
main.js
40
main.js
@ -50,7 +50,7 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
// mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPC handler for selecting directory
|
// IPC handler for selecting directory
|
||||||
@ -309,11 +309,42 @@ ipcMain.handle('begin-mapping', async (event, { directory, files, tvdbId }) => {
|
|||||||
const oldPath = file.filePath;
|
const oldPath = file.filePath;
|
||||||
const ext = path.extname(oldPath);
|
const ext = path.extname(oldPath);
|
||||||
|
|
||||||
// Format episode number with leading zero
|
|
||||||
const epNum = String(file.episodeNumber).padStart(2, '0');
|
|
||||||
const seasonNum = String(seasonNumber).padStart(2, '0');
|
const seasonNum = String(seasonNumber).padStart(2, '0');
|
||||||
|
|
||||||
// Build new filename: "ShowName S01E01 - 1080p.ext"
|
// Handle episode ranges (e.g., "1-3" for multiple episodes)
|
||||||
|
const episodeStart = file.episodeStart || file.episodeNumber || 1;
|
||||||
|
const episodeEnd = file.episodeEnd !== undefined ? file.episodeEnd : episodeStart;
|
||||||
|
|
||||||
|
// If range is valid (start < end), create multi-episode files
|
||||||
|
if (episodeEnd > episodeStart) {
|
||||||
|
// Multi-episode range (e.g., "ShowName S01E01-E03 - 1080p.ext")
|
||||||
|
const startEpNum = String(episodeStart).padStart(2, '0');
|
||||||
|
const endEpNum = String(episodeEnd).padStart(2, '0');
|
||||||
|
|
||||||
|
let newFileName = `${showName} S${seasonNum}E${startEpNum}-${endEpNum}`;
|
||||||
|
if (file.quality && file.quality !== 'N/A' && file.quality !== '-') {
|
||||||
|
newFileName += ` - ${file.quality}`;
|
||||||
|
}
|
||||||
|
newFileName += ext;
|
||||||
|
|
||||||
|
const newPath = path.join(path.dirname(oldPath), newFileName);
|
||||||
|
|
||||||
|
writeLog(`[BEGIN-MAPPING] Processing range: ${episodeStart}-${episodeEnd}, new filename: ${newFileName}`);
|
||||||
|
|
||||||
|
// Check if target already exists
|
||||||
|
if (fs.existsSync(newPath)) {
|
||||||
|
writeLog(`[BEGIN-MAPPING] Target exists, skipping: ${newFileName}`);
|
||||||
|
errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(oldPath, newPath);
|
||||||
|
writeLog(`[BEGIN-MAPPING] Renamed range: ${path.basename(oldPath)} -> ${newFileName}`);
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
// Single episode (original logic)
|
||||||
|
const epNum = String(episodeStart).padStart(2, '0');
|
||||||
|
|
||||||
let newFileName = `${showName} S${seasonNum}E${epNum}`;
|
let newFileName = `${showName} S${seasonNum}E${epNum}`;
|
||||||
if (file.quality && file.quality !== 'N/A' && file.quality !== '-') {
|
if (file.quality && file.quality !== 'N/A' && file.quality !== '-') {
|
||||||
newFileName += ` - ${file.quality}`;
|
newFileName += ` - ${file.quality}`;
|
||||||
@ -339,6 +370,7 @@ ipcMain.handle('begin-mapping', async (event, { directory, files, tvdbId }) => {
|
|||||||
fs.renameSync(oldPath, newPath);
|
fs.renameSync(oldPath, newPath);
|
||||||
writeLog(`[BEGIN-MAPPING] Renamed: ${path.basename(oldPath)} -> ${newFileName}`);
|
writeLog(`[BEGIN-MAPPING] Renamed: ${path.basename(oldPath)} -> ${newFileName}`);
|
||||||
successCount++;
|
successCount++;
|
||||||
|
}
|
||||||
} catch (fileErr) {
|
} catch (fileErr) {
|
||||||
writeLog(`[BEGIN-MAPPING] Error renaming file: ${fileErr.message}`);
|
writeLog(`[BEGIN-MAPPING] Error renaming file: ${fileErr.message}`);
|
||||||
errorCount++;
|
errorCount++;
|
||||||
|
|||||||
237
renderer.js
237
renderer.js
@ -11,6 +11,7 @@ const showDetailsEl = document.getElementById('show-details');
|
|||||||
const progressContainer = document.getElementById('progress-container');
|
const progressContainer = document.getElementById('progress-container');
|
||||||
const progressText = document.getElementById('progress-text');
|
const progressText = document.getElementById('progress-text');
|
||||||
const progressCount = document.getElementById('progress-count');
|
const progressCount = document.getElementById('progress-count');
|
||||||
|
const updateRangesBtn = document.getElementById('update-ranges-btn');
|
||||||
|
|
||||||
// Current state
|
// Current state
|
||||||
let currentDirectory = null;
|
let currentDirectory = null;
|
||||||
@ -24,12 +25,28 @@ let selectedSeasonEpisodeCount = 0;
|
|||||||
selectDirBtn.addEventListener('click', selectDirectory);
|
selectDirBtn.addEventListener('click', selectDirectory);
|
||||||
searchInput.addEventListener('input', debounce(searchShows, 300));
|
searchInput.addEventListener('input', debounce(searchShows, 300));
|
||||||
|
|
||||||
// Add click handler for the floating circle (play button) to move all tagged files
|
// Add click handler for episode number editing (ranges)
|
||||||
document.getElementById('tagged-circle').addEventListener('click', moveAllTaggedFiles);
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.classList.contains('episode-number')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
makeEpisodeRangeEditable(e.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add click handler for Update Ranges button
|
||||||
|
if (updateRangesBtn) {
|
||||||
|
updateRangesBtn.addEventListener('click', function() {
|
||||||
|
updateEpisodeNumbers();
|
||||||
|
checkEpisodeCountMatch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add click handler for Begin Mapping button
|
// Add click handler for Begin Mapping button
|
||||||
document.getElementById('begin-mapping-btn').addEventListener('click', beginMapping);
|
document.getElementById('begin-mapping-btn').addEventListener('click', beginMapping);
|
||||||
|
|
||||||
|
// Add click handler for the floating circle (play button) to move all tagged files
|
||||||
|
document.getElementById('tagged-circle').addEventListener('click', moveAllTaggedFiles);
|
||||||
|
|
||||||
// Listen for scan progress updates from main process
|
// Listen for scan progress updates from main process
|
||||||
ipcRenderer.on('scan-progress', (event, { current, total, fileName }) => {
|
ipcRenderer.on('scan-progress', (event, { current, total, fileName }) => {
|
||||||
updateProgress(current, total, fileName);
|
updateProgress(current, total, fileName);
|
||||||
@ -227,7 +244,11 @@ function displayFiles(files) {
|
|||||||
|
|
||||||
fileItem.innerHTML = `
|
fileItem.innerHTML = `
|
||||||
<div class="drag-handle">⋮⋮</div>
|
<div class="drag-handle">⋮⋮</div>
|
||||||
<div class="episode-number" data-episode="${episodeNumber}">${episodeNumber}</div>
|
<div class="episode-number-container">
|
||||||
|
<button class="episode-arrow episode-arrow-left" data-file-path="${file.path}" title="Decrease episode range">◀</button>
|
||||||
|
<div class="episode-number" data-episode-start="${episodeNumber}" data-episode-end="${episodeNumber}">${episodeNumber}</div>
|
||||||
|
<button class="episode-arrow episode-arrow-right" data-file-path="${file.path}" title="Increase episode range">▶</button>
|
||||||
|
</div>
|
||||||
<div class="file-name" data-file-path="${file.path}">${file.name}</div>
|
<div class="file-name" data-file-path="${file.path}">${file.name}</div>
|
||||||
<div class="file-duration">${duration}</div>
|
<div class="file-duration">${duration}</div>
|
||||||
<div class="file-quality">${quality}</div>
|
<div class="file-quality">${quality}</div>
|
||||||
@ -255,6 +276,24 @@ function displayFiles(files) {
|
|||||||
makeEditable(e.target);
|
makeEditable(e.target);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add click handlers for episode arrow buttons
|
||||||
|
const arrowLeft = fileItem.querySelector('.episode-arrow-left');
|
||||||
|
const arrowRight = fileItem.querySelector('.episode-arrow-right');
|
||||||
|
|
||||||
|
if (arrowLeft) {
|
||||||
|
arrowLeft.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEpisodeArrowClick(this, 'left');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrowRight) {
|
||||||
|
arrowRight.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEpisodeArrowClick(this, 'right');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add hover effects for tags
|
// Add hover effects for tags
|
||||||
const tagIcons = fileItem.querySelectorAll('.tag-icon');
|
const tagIcons = fileItem.querySelectorAll('.tag-icon');
|
||||||
tagIcons.forEach(icon => {
|
tagIcons.forEach(icon => {
|
||||||
@ -413,16 +452,168 @@ function updateEpisodeNumbers() {
|
|||||||
const fileItems = fileListEl.querySelectorAll('.file-item:not(.folder-item)');
|
const fileItems = fileListEl.querySelectorAll('.file-item:not(.folder-item)');
|
||||||
let episodeNum = 1;
|
let episodeNum = 1;
|
||||||
|
|
||||||
fileItems.forEach(item => {
|
console.log('[updateEpisodeNumbers] Starting with episodeNum:', episodeNum);
|
||||||
|
console.log('[updateEpisodeNumbers] Total items:', fileItems.length);
|
||||||
|
console.trace('[updateEpisodeNumbers] Call stack');
|
||||||
|
|
||||||
|
fileItems.forEach((item, index) => {
|
||||||
const episodeEl = item.querySelector('.episode-number');
|
const episodeEl = item.querySelector('.episode-number');
|
||||||
if (episodeEl) {
|
if (episodeEl) {
|
||||||
episodeEl.textContent = episodeNum;
|
const storedStart = episodeEl.dataset.episodeStart;
|
||||||
|
const storedEnd = episodeEl.dataset.episodeEnd;
|
||||||
|
console.log(`[updateEpisodeNumbers] Item ${index}: storedStart="${storedStart}", storedEnd="${storedEnd}"`);
|
||||||
|
|
||||||
|
const start = storedStart ? parseInt(storedStart) : episodeNum;
|
||||||
|
const end = storedEnd ? parseInt(storedEnd) : start;
|
||||||
|
|
||||||
|
const rangeText = start === end ? `${start}` : `${start}-${end}`;
|
||||||
|
episodeEl.textContent = rangeText;
|
||||||
episodeEl.dataset.episode = episodeNum;
|
episodeEl.dataset.episode = episodeNum;
|
||||||
episodeNum++;
|
console.log(`[updateEpisodeNumbers] Item ${index}: calculated start=${start}, end=${end}, display=${rangeText}, next episodeNum=${end + 1}`);
|
||||||
|
episodeNum = end + 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make an episode number editable for range editing
|
||||||
|
function makeEpisodeRangeEditable(element) {
|
||||||
|
if (element.contentEditable === 'true') return;
|
||||||
|
|
||||||
|
const originalText = element.textContent;
|
||||||
|
const originalStart = element.dataset.episodeStart;
|
||||||
|
const originalEnd = element.dataset.episodeEnd;
|
||||||
|
|
||||||
|
element.contentEditable = 'true';
|
||||||
|
element.focus();
|
||||||
|
element.classList.add('editing');
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(element);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
const saveEdit = function() {
|
||||||
|
const newText = element.textContent.trim();
|
||||||
|
|
||||||
|
if (newText !== originalText) {
|
||||||
|
let start, end;
|
||||||
|
|
||||||
|
if (newText.includes('-')) {
|
||||||
|
const parts = newText.split('-').map(p => parseInt(p.trim()));
|
||||||
|
start = parts[0];
|
||||||
|
end = parts[1] || start;
|
||||||
|
} else {
|
||||||
|
const num = parseInt(newText);
|
||||||
|
start = num;
|
||||||
|
end = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNaN(start) && !isNaN(end)) {
|
||||||
|
element.dataset.episodeStart = start;
|
||||||
|
element.dataset.episodeEnd = end;
|
||||||
|
console.log(`[makeEpisodeRangeEditable] Episode range updated: ${start}-${end}`);
|
||||||
|
updateEpisodeNumbers();
|
||||||
|
} else {
|
||||||
|
element.textContent = originalText;
|
||||||
|
element.dataset.episodeStart = originalStart;
|
||||||
|
element.dataset.episodeEnd = originalEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.contentEditable = 'false';
|
||||||
|
element.classList.remove('editing');
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
saveEdit();
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
element.addEventListener('blur', saveEdit, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle episode arrow button clicks
|
||||||
|
function handleEpisodeArrowClick(element, direction) {
|
||||||
|
const fileItem = element.closest('.file-item');
|
||||||
|
const episodeEl = fileItem.querySelector('.episode-number');
|
||||||
|
|
||||||
|
if (!episodeEl) return;
|
||||||
|
|
||||||
|
// Get current values before updating
|
||||||
|
let start = parseInt(episodeEl.dataset.episodeStart) || 1;
|
||||||
|
let end = parseInt(episodeEl.dataset.episodeEnd) || start;
|
||||||
|
|
||||||
|
const oldStart = start;
|
||||||
|
const oldEnd = end;
|
||||||
|
|
||||||
|
// Update the episode range
|
||||||
|
if (direction === 'left') {
|
||||||
|
// Decrease range - move start back by 1
|
||||||
|
if (start > 1) {
|
||||||
|
start--;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Increase range - move end forward by 1
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update this episode's data attributes
|
||||||
|
episodeEl.dataset.episodeStart = start;
|
||||||
|
episodeEl.dataset.episodeEnd = end;
|
||||||
|
|
||||||
|
// Get all media file items (not folders) in order
|
||||||
|
const allFileItems = Array.from(fileListEl.querySelectorAll('.file-item:not(.folder-item)'));
|
||||||
|
|
||||||
|
// Find current index by matching file path
|
||||||
|
const filePath = fileItem.dataset.filePath;
|
||||||
|
let currentIndex = -1;
|
||||||
|
for (let i = 0; i < allFileItems.length; i++) {
|
||||||
|
if (allFileItems[i].dataset.filePath === filePath) {
|
||||||
|
currentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
// For right arrow: shift all subsequent episodes forward by 1
|
||||||
|
if (direction === 'right') {
|
||||||
|
for (let i = currentIndex + 1; i < allFileItems.length; i++) {
|
||||||
|
const nextItem = allFileItems[i];
|
||||||
|
const nextEpisodeEl = nextItem.querySelector('.episode-number');
|
||||||
|
|
||||||
|
if (nextEpisodeEl) {
|
||||||
|
let nextStart = parseInt(nextEpisodeEl.dataset.episodeStart) || 1;
|
||||||
|
const nextEnd = parseInt(nextEpisodeEl.dataset.episodeEnd) || nextStart;
|
||||||
|
|
||||||
|
nextEpisodeEl.dataset.episodeStart = nextStart + 1;
|
||||||
|
nextEpisodeEl.dataset.episodeEnd = nextEnd + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For left arrow: all subsequent episodes shift down
|
||||||
|
const shiftAmount = oldStart - start;
|
||||||
|
for (let i = currentIndex + 1; i < allFileItems.length; i++) {
|
||||||
|
const nextItem = allFileItems[i];
|
||||||
|
const nextEpisodeEl = nextItem.querySelector('.episode-number');
|
||||||
|
|
||||||
|
if (nextEpisodeEl) {
|
||||||
|
let nextStart = parseInt(nextEpisodeEl.dataset.episodeStart) || 1;
|
||||||
|
const nextEnd = parseInt(nextEpisodeEl.dataset.episodeEnd) || nextStart;
|
||||||
|
|
||||||
|
nextEpisodeEl.dataset.episodeStart = nextStart - shiftAmount;
|
||||||
|
nextEpisodeEl.dataset.episodeEnd = nextEnd - shiftAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force immediate DOM update
|
||||||
|
updateEpisodeNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
// Make a file name editable
|
// Make a file name editable
|
||||||
function makeEditable(element) {
|
function makeEditable(element) {
|
||||||
// Prevent editing if already in edit mode
|
// Prevent editing if already in edit mode
|
||||||
@ -655,7 +846,7 @@ async function beginMapping() {
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = 'Mapping...';
|
btn.textContent = 'Mapping...';
|
||||||
|
|
||||||
// Collect file data with episode numbers from UI order
|
// Collect file data with episode ranges from UI order
|
||||||
const filesToMap = [];
|
const filesToMap = [];
|
||||||
fileItems.forEach((item, index) => {
|
fileItems.forEach((item, index) => {
|
||||||
const fileNameEl = item.querySelector('.file-name');
|
const fileNameEl = item.querySelector('.file-name');
|
||||||
@ -663,10 +854,14 @@ async function beginMapping() {
|
|||||||
const episodeNumEl = item.querySelector('.episode-number');
|
const episodeNumEl = item.querySelector('.episode-number');
|
||||||
|
|
||||||
if (fileNameEl && fileNameEl.dataset.filePath) {
|
if (fileNameEl && fileNameEl.dataset.filePath) {
|
||||||
|
const episodeStart = episodeNumEl ? parseInt(episodeNumEl.dataset.episodeStart || (index + 1)) : (index + 1);
|
||||||
|
const episodeEnd = episodeNumEl ? parseInt(episodeNumEl.dataset.episodeEnd || episodeStart) : episodeStart;
|
||||||
|
|
||||||
filesToMap.push({
|
filesToMap.push({
|
||||||
filePath: fileNameEl.dataset.filePath,
|
filePath: fileNameEl.dataset.filePath,
|
||||||
quality: qualityEl ? qualityEl.textContent : '',
|
quality: qualityEl ? qualityEl.textContent : '',
|
||||||
episodeNumber: episodeNumEl ? parseInt(episodeNumEl.textContent) : (index + 1)
|
episodeStart: episodeStart,
|
||||||
|
episodeEnd: episodeEnd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -847,9 +1042,31 @@ function checkEpisodeCountMatch() {
|
|||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
// Count only actual media files (not folders)
|
// Count only actual media files (not folders)
|
||||||
const mediaFiles = document.querySelectorAll('.file-item:not(.folder-item)');
|
const mediaFiles = document.querySelectorAll('.file-item:not(.folder-item)');
|
||||||
const mediaCount = mediaFiles.length;
|
|
||||||
|
|
||||||
if (selectedSeasonEpisodeCount > 0 && mediaCount === selectedSeasonEpisodeCount) {
|
// Calculate total episode range (sum of all episode ranges)
|
||||||
|
let totalEpisodeCount = 0;
|
||||||
|
let lastEpisodeEnd = 0;
|
||||||
|
|
||||||
|
mediaFiles.forEach((item, index) => {
|
||||||
|
const episodeEl = item.querySelector('.episode-number');
|
||||||
|
if (episodeEl) {
|
||||||
|
const episodeStart = parseInt(episodeEl.dataset.episodeStart || (index + 1));
|
||||||
|
const episodeEnd = parseInt(episodeEl.dataset.episodeEnd || episodeStart);
|
||||||
|
|
||||||
|
// Calculate episodes in this range
|
||||||
|
const rangeSize = episodeEnd - episodeStart + 1;
|
||||||
|
totalEpisodeCount += rangeSize;
|
||||||
|
|
||||||
|
// Track the last episode end for sequential checking
|
||||||
|
if (index === 0 || episodeEnd > lastEpisodeEnd) {
|
||||||
|
lastEpisodeEnd = episodeEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use lastEpisodeEnd for comparison (handles ranges properly)
|
||||||
|
// For example: if we have episodes 1-3, 4-5, the last episode is 5, not 2 files
|
||||||
|
if (selectedSeasonEpisodeCount > 0 && lastEpisodeEnd === selectedSeasonEpisodeCount) {
|
||||||
// Perfect match - add green outline
|
// Perfect match - add green outline
|
||||||
fileList.style.border = '3px solid #28a745';
|
fileList.style.border = '3px solid #28a745';
|
||||||
fileList.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.4)';
|
fileList.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.4)';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user