diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4778f48..21bdef4 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -21,16 +21,6 @@ jobs: git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true - - name: Run ruff (Python lint) - if: always() - run: | - if [[ -f pyproject.toml ]]; then - pip3 install ruff - ruff check . - else - echo "No Python project detected, skipping ruff" - fi - - name: Run npm lint (JS/TS) if: always() run: | @@ -52,36 +42,18 @@ jobs: git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true - - name: Run pytest (Python) - if: always() - run: | - if [[ -f pyproject.toml ]]; then - python3 -m pip install --upgrade pip - pip3 install -e ".[dev]" 2>/dev/null || pip3 install -e . 2>/dev/null || true - pip3 install pytest - pytest tests/ -v --tb=short 2>/dev/null || true - else - echo "No Python project detected, skipping pytest" - fi + - name: Install dependencies + run: npm ci - - name: Run npm test (JS/TS) - if: always() - run: | - if [[ -f package.json ]]; then - npm ci - npm run test --if-present || true - else - echo "No Node.js project detected, skipping npm test" - fi + - name: Run tests with coverage + run: npm test - - name: Run Go tests + - name: Upload coverage report if: always() - run: | - if [[ -f go.mod ]]; then - go test ./... - else - echo "No Go project detected, skipping go test" - fi + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ docker-build: runs-on: ubuntu-latest @@ -112,16 +84,6 @@ jobs: git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true - - name: Run bandit (Python SAST) - if: always() - run: | - if [[ -f pyproject.toml ]]; then - pip3 install bandit - bandit -r . --severity-level high --confidence-level high --exclude tests/,test_* - else - echo "No Python project detected, skipping bandit" - fi - - name: Run npm audit (JS/TS) if: always() run: | diff --git a/.gitignore b/.gitignore index a5fb3cf..86a248c 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,7 @@ dist/ # Temporary files *.tmp -*.temp \ No newline at end of file +*.temp + +# Coverage +coverage/ \ No newline at end of file diff --git a/coverage/AppState.js.html b/coverage/AppState.js.html new file mode 100644 index 0000000..1fc60d4 --- /dev/null +++ b/coverage/AppState.js.html @@ -0,0 +1,697 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 | 1x + +1x +1x +1x +1x +1x +22x +22x +22x +22x +22x +22x +22x +22x +22x +22x +22x +22x + +1x +1x +1x +1x +1x +4x +4x + +1x +1x +1x +1x +1x +4x +4x + +1x +1x +1x +1x +1x +3x +3x + +1x +1x +1x +1x +1x +4x +4x + +1x +1x +1x +1x +1x +2x +2x + +1x +1x +1x +1x +1x +3x +3x + +1x +1x +1x +1x +1x +2x +2x + +1x +1x +1x +1x +1x +3x +3x + +1x +1x +1x +1x +1x +2x +2x + +1x +1x +1x +1x +1x +3x +3x + +1x +1x +1x +1x +1x +2x +2x + +1x +1x +1x +1x +1x +3x +3x + +1x +1x +1x +1x +1x + + + +1x +1x +1x +1x +1x +3x +3x + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + +1x +1x +1x +1x +1x +22x +22x +22x +22x +22x + +1x +1x +1x +1x +1x +5x +3x +3x +3x +3x +2x +5x + +1x +1x +1x +1x +1x +9x +9x + +1x +1x +1x +1x +1x +2x +2x + +1x +1x +1x +1x +1x +5x +5x + +1x +1x +1x +1x +1x +6x +6x +1x + +1x | const path = require('path');
+
+/**
+ * AppState - Manages application state
+ */
+class AppState {
+ constructor() {
+ this.currentDirectory = null;
+ this.currentFiles = [];
+ this.currentShow = null;
+ this.currentSeasons = [];
+ this.currentEpisodes = [];
+ this.selectedSeasonEpisodeCount = 0;
+ this.isUpdatingEpisodeNumbers = false;
+
+ // Navigation stack for folder navigation
+ this.navigationStack = [];
+ this.currentDepth = 0;
+ }
+
+ /**
+ * Set the current directory
+ * @param {string} directory - Directory path
+ */
+ setCurrentDirectory(directory) {
+ this.currentDirectory = directory;
+ }
+
+ /**
+ * Get the current directory
+ * @returns {string|null} Current directory path
+ */
+ getCurrentDirectory() {
+ return this.currentDirectory;
+ }
+
+ /**
+ * Set current files
+ * @param {Array} files - Array of file objects
+ */
+ setCurrentFiles(files) {
+ this.currentFiles = files;
+ }
+
+ /**
+ * Get current files
+ * @returns {Array} Array of file objects
+ */
+ getCurrentFiles() {
+ return this.currentFiles;
+ }
+
+ /**
+ * Set current show
+ * @param {Object} show - Show object
+ */
+ setCurrentShow(show) {
+ this.currentShow = show;
+ }
+
+ /**
+ * Get current show
+ * @returns {Object|null} Current show object
+ */
+ getCurrentShow() {
+ return this.currentShow;
+ }
+
+ /**
+ * Set current seasons
+ * @param {Array} seasons - Array of season objects
+ */
+ setCurrentSeasons(seasons) {
+ this.currentSeasons = seasons;
+ }
+
+ /**
+ * Get current seasons
+ * @returns {Array} Array of season objects
+ */
+ getCurrentSeasons() {
+ return this.currentSeasons;
+ }
+
+ /**
+ * Set current episodes
+ * @param {Array} episodes - Array of episode objects
+ */
+ setCurrentEpisodes(episodes) {
+ this.currentEpisodes = episodes;
+ }
+
+ /**
+ * Get current episodes
+ * @returns {Array} Array of episode objects
+ */
+ getCurrentEpisodes() {
+ return this.currentEpisodes;
+ }
+
+ /**
+ * Set selected season episode count
+ * @param {number} count - Episode count
+ */
+ setSelectedSeasonEpisodeCount(count) {
+ this.selectedSeasonEpisodeCount = count;
+ }
+
+ /**
+ * Get selected season episode count
+ * @returns {number} Episode count
+ */
+ getSelectedSeasonEpisodeCount() {
+ return this.selectedSeasonEpisodeCount;
+ }
+
+ /**
+ * Check if episode numbers are currently updating
+ * @returns {boolean} True if updating
+ */
+ isUpdatingEpisodeNumbers() {
+ return this.isUpdatingEpisodeNumbers;
+ }
+
+ /**
+ * Set episode numbers updating flag
+ * @param {boolean} updating - Updating state
+ */
+ setUpdatingEpisodeNumbers(updating) {
+ this.isUpdatingEpisodeNumbers = updating;
+ }
+
+ /**
+ * Reset state to initial values
+ */
+ reset() {
+ this.currentDirectory = null;
+ this.currentFiles = [];
+ this.currentShow = null;
+ this.currentSeasons = [];
+ this.currentEpisodes = [];
+ this.selectedSeasonEpisodeCount = 0;
+ this.isUpdatingEpisodeNumbers = false;
+ this.navigationStack = [];
+ this.currentDepth = 0;
+ }
+
+ /**
+ * Add directory to navigation stack
+ * @param {string} directory - Directory path to add
+ */
+ addToNavigationStack(directory) {
+ // Remove any forward history if navigating from middle of stack
+ this.navigationStack = this.navigationStack.slice(0, this.currentDepth + 1);
+ this.navigationStack.push(directory);
+ this.currentDepth = this.navigationStack.length - 1;
+ }
+
+ /**
+ * Go back to previous directory in navigation stack
+ * @returns {string|null} Previous directory path or null if at root
+ */
+ goBack() {
+ if (this.canGoBack()) {
+ this.navigationStack.pop();
+ this.currentDepth--;
+ return this.navigationStack[this.currentDepth];
+ }
+ return null;
+ }
+
+ /**
+ * Check if can go back in navigation history
+ * @returns {boolean} True if can go back
+ */
+ canGoBack() {
+ return this.currentDepth > 0;
+ }
+
+ /**
+ * Get current directory from navigation stack
+ * @returns {string|null} Current directory path
+ */
+ getCurrentDirectoryFromStack() {
+ return this.navigationStack[this.currentDepth] || null;
+ }
+
+ /**
+ * Get navigation depth
+ * @returns {number} Current navigation depth
+ */
+ getNavigationDepth() {
+ return this.currentDepth;
+ }
+
+ /**
+ * Get navigation stack
+ * @returns {Array} Array of visited directories
+ */
+ getNavigationStack() {
+ return this.navigationStack;
+ }
+}
+
+module.exports = AppState; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 | 1x +1x +1x +1x +1x +21x +21x + +1x +1x +1x +1x +1x +6x +1x +1x +1x + +5x + +5x +5x +5x + +5x +6x +6x +6x +6x + +6x +6x + +6x +6x +6x +6x +6x +5x +5x +5x +5x +6x + +1x +1x +1x +1x +1x +2x + +1x +1x +1x + +1x +1x +1x + +1x +1x +1x +1x +1x + +1x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1x + +1x + + + + +1x + +1x +2x + +1x +1x +1x +1x +1x +1x +1x +4x +4x + +4x + +3x +4x +4x + +4x +4x + +4x +4x +2x +2x +1x +1x +4x +1x +1x +1x + +3x +3x +3x + +3x +3x + +3x +3x +3x +3x +3x +3x +3x +3x +3x + +4x + +3x +4x +1x + + + + + + + + + + + +4x +2x +2x +2x + + + + + + + + + + + +2x + +3x +3x +4x + +1x +1x +1x +1x +1x +1x +1x +2x + +1x + +1x +3x +3x + +3x +3x + +3x +2x +2x +3x +1x +2x + +1x +1x +1x +1x +1x +2x + +1x + +1x +1x +1x +2x + +1x +1x +1x +1x +1x +1x +4x +4x +4x +4x + +1x +1x +1x +1x +1x +1x +2x +2x + +2x +2x +2x +2x +2x +2x +2x + +2x +2x + +1x +1x +1x +1x +1x +1x +2x +2x + +2x +1x +1x +1x +1x +1x +1x +1x +2x + +2x +2x + +1x +1x +1x +1x +1x +4x +4x + +1x +1x +1x +1x +1x +3x +3x +1x + +1x | /**
+ * EpisodeManager - Manages episode number editing and highlighting
+ */
+class EpisodeManager {
+ constructor() {
+ this.isUpdatingEpisodeNumbers = false;
+ }
+
+ /**
+ * Update episode numbers in the file list
+ * @param {HTMLElement} fileListEl - File list element
+ */
+ updateEpisodeNumbers(fileListEl) {
+ if (this.isUpdatingEpisodeNumbers) {
+ console.log('[updateEpisodeNumbers] Already updating, skipping re-entrant call');
+ return;
+ }
+
+ this.isUpdatingEpisodeNumbers = true;
+
+ try {
+ const fileItems = fileListEl.querySelectorAll('.file-item:not(.folder-item)');
+ let episodeNum = 1;
+
+ fileItems.forEach((item, index) => {
+ const episodeEl = item.querySelector('.episode-number');
+ if (episodeEl) {
+ const storedStart = episodeEl.dataset.episodeStart;
+ const storedEnd = episodeEl.dataset.episodeEnd;
+
+ 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;
+ episodeNum = end + 1;
+ }
+ });
+ } finally {
+ this.isUpdatingEpisodeNumbers = false;
+ }
+ }
+
+ /**
+ * Make episode range editable
+ * @param {HTMLElement} element - Episode number element
+ */
+ 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 = () => {
+ 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(`Episode range updated: ${start}-${end}`);
+ this.updateEpisodeNumbers(element.closest('#file-list'));
+ } 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 click
+ * @param {HTMLElement} element - Arrow element
+ * @param {string} direction - Direction (left or right)
+ * @param {HTMLElement} fileListEl - File list element
+ */
+ handleEpisodeArrowClick(element, direction, fileListEl) {
+ 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
+ this.updateEpisodeNumbers(fileListEl);
+ }
+
+ /**
+ * Highlight episodes in the episodes container
+ * @param {number} startEpisode - Start episode number
+ * @param {number} endEpisode - End episode number
+ * @param {HTMLElement} episodesContainer - Episodes container element
+ */
+ highlightEpisodes(startEpisode, endEpisode, episodesContainer) {
+ if (!episodesContainer) return;
+
+ const episodeItems = episodesContainer.querySelectorAll('.episode-item');
+
+ episodeItems.forEach(item => {
+ const episodeText = item.textContent.trim();
+ const episodeMatch = episodeText.match(/E(\d+)/);
+
+ if (episodeMatch) {
+ const episodeNum = parseInt(episodeMatch[1]);
+
+ if (episodeNum >= startEpisode && episodeNum <= endEpisode) {
+ item.classList.add('highlighted-episode');
+ }
+ }
+ });
+ }
+
+ /**
+ * Remove highlights from all episodes
+ * @param {HTMLElement} episodesContainer - Episodes container element
+ */
+ removeEpisodeHighlights(episodesContainer) {
+ if (!episodesContainer) return;
+
+ const episodeItems = episodesContainer.querySelectorAll('.episode-item');
+
+ episodeItems.forEach(item => {
+ item.classList.remove('highlighted-episode');
+ });
+ }
+
+ /**
+ * Get episode range from element
+ * @param {HTMLElement} episodeEl - Episode element
+ * @returns {Object} Episode range object
+ */
+ getEpisodeRange(episodeEl) {
+ const episodeStart = parseInt(episodeEl.dataset.episodeStart) || 1;
+ const episodeEnd = parseInt(episodeEl.dataset.episodeEnd) || episodeStart;
+ return { start: episodeStart, end: episodeEnd };
+ }
+
+ /**
+ * Calculate total episode count from file items
+ * @param {HTMLElement} fileListEl - File list element
+ * @returns {number} Total episode count
+ */
+ calculateTotalEpisodeCount(fileListEl) {
+ const mediaFiles = fileListEl.querySelectorAll('.file-item:not(.folder-item)');
+ let totalEpisodeCount = 0;
+
+ mediaFiles.forEach(item => {
+ const episodeEl = item.querySelector('.episode-number');
+ if (episodeEl) {
+ const { start, end } = this.getEpisodeRange(episodeEl);
+ totalEpisodeCount += (end - start + 1);
+ }
+ });
+
+ return totalEpisodeCount;
+ }
+
+ /**
+ * Get last episode end from file items
+ * @param {HTMLElement} fileListEl - File list element
+ * @returns {number} Last episode end number
+ */
+ getLastEpisodeEnd(fileListEl) {
+ const mediaFiles = fileListEl.querySelectorAll('.file-item:not(.folder-item)');
+ let lastEpisodeEnd = 0;
+
+ mediaFiles.forEach((item, index) => {
+ const episodeEl = item.querySelector('.episode-number');
+ if (episodeEl) {
+ const episodeEnd = parseInt(episodeEl.dataset.episodeEnd) || 1;
+ if (index === 0 || episodeEnd > lastEpisodeEnd) {
+ lastEpisodeEnd = episodeEnd;
+ }
+ }
+ });
+
+ return lastEpisodeEnd;
+ }
+
+ /**
+ * Get updating episode numbers flag
+ * @returns {boolean} Updating state
+ */
+ getIsUpdatingEpisodeNumbers() {
+ return this.isUpdatingEpisodeNumbers;
+ }
+
+ /**
+ * Set updating episode numbers flag
+ * @param {boolean} updating - Updating state
+ */
+ setIsUpdatingEpisodeNumbers(updating) {
+ this.isUpdatingEpisodeNumbers = updating;
+ }
+}
+
+module.exports = EpisodeManager; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 | 1x +1x +1x +1x +1x +18x +18x +18x +18x +18x +18x + +1x +1x +1x +1x +1x +1x +1x + +1x +1x +1x +1x +1x +14x +14x +14x +14x + +14x +1x +1x +1x + +13x +13x +13x + +13x +13x + +13x +13x +6x +13x + +13x +13x +16x +16x +13x +14x + +1x +1x +1x +1x +1x +1x +6x +6x + +6x +6x +6x + +6x +6x +6x + +6x +6x + +6x +6x + +6x +6x + +6x +6x + +6x + +6x +6x +6x +1x + + +1x +1x +1x +6x + +6x +6x + +1x +1x +1x +1x +1x +1x +1x +16x +16x +16x +16x +16x + +16x +16x +16x + +16x +16x +16x + +16x +16x + +16x +16x +16x +16x + +16x +16x +16x +16x +16x + +16x +16x +16x +16x + +16x + +16x +16x +16x + +16x +1x +1x +1x +1x +1x + +16x +16x +16x + +16x +16x +16x + +16x +16x +16x + +16x +16x + +16x +16x +16x +16x + +16x +16x +16x +16x + +16x +16x +16x +16x + +16x +16x +16x +16x + +16x +16x +16x +16x +16x + +16x + +16x + +16x +16x +16x +16x +16x + +16x +16x + +1x +1x +1x +1x +1x +1x + + + + + + + +1x +1x +1x +1x +1x +1x + + + + + + + + + +1x +1x +1x +1x +1x +1x + + + + + + + + + + +1x +1x +1x +1x +1x +1x + + + + +1x +1x +1x +1x +1x + + + + + + + + + + + + + + + + + + + + + + + + +1x +1x +1x +1x +1x +1x + +1x +3x +3x +3x +3x + +3x +3x + +3x +3x +3x +3x +3x +1x +1x + +1x +1x +1x +1x +1x +1x + +1x +1x +1x +1x +1x +9x +9x + +1x +1x +1x +1x +1x +1x +1x + +1x +1x +1x +1x +1x +32x +32x + + + + + + + + + + + + + + + + +32x + +32x + + + + + + + + + + +32x +32x + +1x +1x +1x +1x +1x +1x +3x +3x +6x +6x + +6x +6x + +6x +6x + +6x +3x +3x +3x +3x +3x +3x + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + +1x | /**
+ * FileListManager - Handles file list display and manipulation
+ */
+class FileListManager {
+ constructor(fileListEl, onFolderClick = null) {
+ this.fileListEl = fileListEl;
+ this.draggedItem = null;
+ this.onFolderClick = onFolderClick || null;
+ this.hoveredEpisodeRange = null;
+ this._setupHoverHandlers();
+ }
+
+ /**
+ * Set callback for folder click events
+ * @param {Function} callback - Function to call when folder is clicked
+ */
+ setFolderClickCallback(callback) {
+ this.onFolderClick = callback;
+ }
+
+ /**
+ * Display files in the UI
+ * @param {Array} files - Array of file objects
+ */
+ displayFiles(files) {
+ // Re-setup hover handlers when displaying files
+ this._setupHoverHandlers();
+
+ this.fileListEl.innerHTML = '';
+
+ if (files.length === 0) {
+ this.fileListEl.innerHTML = '<p>No media files found in this directory.</p>';
+ return;
+ }
+
+ // Separate folders from media files
+ const folders = files.filter(f => f.isFolder);
+ const mediaFiles = files.filter(f => !f.isFolder);
+
+ // Track episode number for media files only
+ let episodeNumber = 1;
+
+ // Display folders first (not draggable, no episode number)
+ folders.forEach(file => {
+ this._createFolderElement(file);
+ });
+
+ // Display media files with episode numbers and drag/drop
+ mediaFiles.forEach((file, index) => {
+ this._createMediaFileElement(file, episodeNumber, index);
+ episodeNumber++;
+ });
+ }
+
+ /**
+ * Create folder element
+ * @param {Object} file - File object
+ * @private
+ */
+ _createFolderElement(file) {
+ const fileItem = document.createElement('div');
+ fileItem.className = 'file-item folder-item';
+
+ const icon = document.createElement('div');
+ icon.style.cssText = 'font-size: 18px; margin-right: 10px;';
+ icon.textContent = '\uD83D\uDCC1';
+
+ const nameEl = document.createElement('div');
+ nameEl.className = 'file-name folder-name';
+ nameEl.textContent = file.name;
+
+ const durationEl = document.createElement('div');
+ durationEl.className = 'file-duration';
+
+ const qualityEl = document.createElement('div');
+ qualityEl.className = 'file-quality';
+
+ const fpsEl = document.createElement('div');
+ fpsEl.className = 'file-fps';
+
+ const tagsEl = document.createElement('div');
+ tagsEl.className = 'file-tags';
+
+ fileItem.append(icon, nameEl, durationEl, qualityEl, fpsEl, tagsEl);
+
+ const filePath = file.path;
+ const fileName = file.name;
+ fileItem.addEventListener('click', (e) => {
+ if (e.target !== fileItem && e.target.className !== 'file-name folder-name') {
+ return;
+ }
+ if (this.onFolderClick) {
+ this.onFolderClick(filePath, fileName);
+ }
+ });
+
+ this.fileListEl.appendChild(fileItem);
+ }
+
+ /**
+ * Create media file element
+ * @param {Object} file - File object
+ * @param {number} episodeNumber - Episode number
+ * @param {number} index - File index
+ */
+ _createMediaFileElement(file, episodeNumber, index) {
+ const fileItem = document.createElement('div');
+ fileItem.className = 'file-item';
+ fileItem.draggable = true;
+ fileItem.dataset.index = index;
+ fileItem.dataset.filePath = file.path;
+
+ const duration = file.duration || '00:00';
+ const quality = file.quality || 'unknown';
+ const fps = file.fps || 'unknown';
+
+ const dragHandle = document.createElement('div');
+ dragHandle.className = 'drag-handle';
+ dragHandle.textContent = '\u22EE\u22EE';
+
+ const episodeContainer = document.createElement('div');
+ episodeContainer.className = 'episode-number-container';
+
+ const leftArrow = document.createElement('button');
+ leftArrow.className = 'episode-arrow episode-arrow-left';
+ leftArrow.title = 'Decrease episode range';
+ leftArrow.textContent = '\u25C0';
+
+ const episodeNum = document.createElement('div');
+ episodeNum.className = 'episode-number';
+ episodeNum.dataset.episodeStart = episodeNumber;
+ episodeNum.dataset.episodeEnd = episodeNumber;
+ episodeNum.textContent = episodeNumber;
+
+ const rightArrow = document.createElement('button');
+ rightArrow.className = 'episode-arrow episode-arrow-right';
+ rightArrow.title = 'Increase episode range';
+ rightArrow.textContent = '\u25B6';
+
+ episodeContainer.append(leftArrow, episodeNum, rightArrow);
+
+ const nameEl = document.createElement('div');
+ nameEl.className = 'file-name';
+ nameEl.textContent = file.name;
+
+ if (file.isProblematic) {
+ const warn = document.createElement('span');
+ warn.className = 'problematic-label';
+ warn.textContent = '\u26A0\ufe0f';
+ nameEl.prepend(warn, ' ');
+ }
+
+ const durEl = document.createElement('div');
+ durEl.className = 'file-duration';
+ durEl.textContent = duration;
+
+ const qualEl = document.createElement('div');
+ qualEl.className = 'file-quality';
+ qualEl.textContent = quality;
+
+ const fpsEl = document.createElement('div');
+ fpsEl.className = 'file-fps';
+ fpsEl.textContent = fps;
+
+ const tagsEl = document.createElement('div');
+ tagsEl.className = 'file-tags';
+
+ const extraTag = document.createElement('span');
+ extraTag.className = 'tag-icon extra-tag';
+ extraTag.title = 'Mark as Extra';
+ extraTag.textContent = '\uD83C\uDFF7\uFE0F';
+
+ const btsTag = document.createElement('span');
+ btsTag.className = 'tag-icon behind-the-scenes-tag';
+ btsTag.title = 'Add Behind the Scenes';
+ btsTag.textContent = '\uD83C\uDFA5';
+
+ const delTag = document.createElement('span');
+ delTag.className = 'tag-icon delete-tag';
+ delTag.title = 'Mark for Deletion';
+ delTag.textContent = '\uD83D\uDDD1\uFE0F';
+
+ const previewBtn = document.createElement('span');
+ previewBtn.className = 'video-preview-btn';
+ previewBtn.title = 'Preview Video';
+ previewBtn.textContent = '\uD83C\uDFAC';
+
+ const playBtn = document.createElement('button');
+ playBtn.className = 'play-button';
+ playBtn.style.cssText = 'opacity: 0.3; cursor: default; flex-shrink: 0;';
+ playBtn.disabled = true;
+ playBtn.textContent = '\u25B6\uFE0F';
+
+ tagsEl.append(extraTag, btsTag, delTag, previewBtn, playBtn);
+
+ fileItem.append(dragHandle, episodeContainer, nameEl, durEl, qualEl, fpsEl, tagsEl);
+
+ fileItem.addEventListener('dragstart', this._handleDragStart.bind(this));
+ fileItem.addEventListener('dragend', this._handleDragEnd.bind(this));
+ fileItem.addEventListener('dragover', this._handleDragOver.bind(this));
+ fileItem.addEventListener('dragleave', this._handleDragLeave.bind(this));
+ fileItem.addEventListener('drop', this._handleDrop.bind(this));
+
+ this.fileListEl.appendChild(fileItem);
+ }
+
+ /**
+ * Handle drag start
+ * @param {Event} e - Drag event
+ * @private
+ */
+ _handleDragStart(e) {
+ const fileItem = this;
+ this.draggedItem = fileItem;
+ fileItem.classList.add('dragging');
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/plain', fileItem.dataset.index);
+ }
+
+ /**
+ * Handle drag end
+ * @param {Event} e - Drag event
+ * @private
+ */
+ _handleDragEnd(e) {
+ const fileItem = this;
+ fileItem.classList.remove('dragging');
+ // Remove drag-over class from all items
+ document.querySelectorAll('.file-item').forEach(item => {
+ item.classList.remove('drag-over');
+ });
+ this.draggedItem = null;
+ }
+
+ /**
+ * Handle drag over
+ * @param {Event} e - Drag event
+ * @private
+ */
+ _handleDragOver(e) {
+ const fileItem = this;
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+
+ // Only show drag-over for non-folder items
+ if (!fileItem.classList.contains('folder-item') && fileItem !== this.draggedItem) {
+ fileItem.classList.add('drag-over');
+ }
+ }
+
+ /**
+ * Handle drag leave
+ * @param {Event} e - Drag event
+ * @private
+ */
+ _handleDragLeave(e) {
+ const fileItem = this;
+ fileItem.classList.remove('drag-over');
+ }
+
+ /**
+ * Handle drop
+ * @param {Event} e - Drop event
+ */
+ _handleDrop(e) {
+ const fileItem = this;
+ e.preventDefault();
+ fileItem.classList.remove('drag-over');
+
+ if (fileItem === this.draggedItem || fileItem.classList.contains('folder-item')) return;
+
+ // Get all media file items (not folders)
+ const fileItems = Array.from(this.fileListEl.querySelectorAll('.file-item:not(.folder-item)'));
+ const draggedIndex = fileItems.indexOf(this.draggedItem);
+ const dropIndex = fileItems.indexOf(fileItem);
+
+ if (draggedIndex === -1 || dropIndex === -1) return;
+
+ // Move the dragged item in the DOM
+ if (draggedIndex < dropIndex) {
+ fileItem.parentNode.insertBefore(this.draggedItem, fileItem.nextSibling);
+ } else {
+ fileItem.parentNode.insertBefore(this.draggedItem, fileItem);
+ }
+
+ // Update episode numbers
+ this.updateEpisodeNumbers();
+ }
+
+ /**
+ * Update episode numbers
+ */
+ updateEpisodeNumbers() {
+ const fileItems = this.fileListEl.querySelectorAll('.file-item:not(.folder-item)');
+ let episodeNum = 1;
+
+ fileItems.forEach((item, index) => {
+ const episodeEl = item.querySelector('.episode-number');
+ if (episodeEl) {
+ const storedStart = episodeEl.dataset.episodeStart;
+ const storedEnd = episodeEl.dataset.episodeEnd;
+
+ 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;
+ episodeNum = end + 1;
+ }
+ });
+ }
+
+ /**
+ * Clear the file list
+ */
+ clear() {
+ this.fileListEl.innerHTML = '';
+ }
+
+ /**
+ * Get all media file items
+ * @returns {NodeList} Media file items
+ */
+ getMediaFileItems() {
+ return this.fileListEl.querySelectorAll('.file-item:not(.folder-item)');
+ }
+
+ /**
+ * Get folder items
+ * @returns {NodeList} Folder items
+ */
+ getFolderItems() {
+ return this.fileListEl.querySelectorAll('.folder-item');
+ }
+
+ /**
+ * Set up hover handlers for file items
+ * @private
+ */
+ _setupHoverHandlers() {
+ // Use event delegation on the file list
+ this.fileListEl.addEventListener('mouseenter', (e) => {
+ const fileItem = e.target.closest('.file-item');
+ if (!fileItem || fileItem.classList.contains('folder-item')) return;
+
+ const episodeEl = fileItem.querySelector('.episode-number');
+ if (!episodeEl) return;
+
+ const startEpisode = parseInt(episodeEl.dataset.episodeStart) || 1;
+ const endEpisode = parseInt(episodeEl.dataset.episodeEnd) || startEpisode;
+
+ // Store hovered range for later use
+ this.hoveredEpisodeRange = { start: startEpisode, end: endEpisode };
+
+ // Notify UIManager to highlight episodes
+ if (this.onHoverStart && typeof this.onHoverStart === 'function') {
+ this.onHoverStart(startEpisode, endEpisode, fileItem);
+ }
+ });
+
+ this.fileListEl.addEventListener('mouseleave', (e) => {
+ const fileItem = e.target.closest('.file-item');
+ if (!fileItem) return;
+
+ // Remove from current file item
+ fileItem.classList.remove('hovered-file');
+
+ // Notify UIManager to remove highlights
+ if (this.onHoverEnd && typeof this.onHoverEnd === 'function') {
+ this.onHoverEnd(fileItem);
+ }
+ });
+ }
+
+ /**
+ * Highlight files matching episode range
+ * @param {number} startEpisode - Start episode number
+ * @param {number} endEpisode - End episode number
+ */
+ highlightFilesMatchingEpisode(startEpisode, endEpisode) {
+ const mediaFiles = this.getMediaFileItems();
+ mediaFiles.forEach(fileItem => {
+ const episodeEl = fileItem.querySelector('.episode-number');
+ if (!episodeEl) return;
+
+ const fileStart = parseInt(episodeEl.dataset.episodeStart) || 1;
+ const fileEnd = parseInt(episodeEl.dataset.episodeEnd) || fileStart;
+
+ // Check if the file's episode range overlaps with the hovered range
+ const overlaps = !(endEpisode < fileStart || startEpisode > fileEnd);
+
+ if (overlaps) {
+ fileItem.classList.add('hovered-file');
+ } else {
+ fileItem.classList.remove('hovered-file');
+ }
+ });
+ }
+
+ /**
+ * Remove file highlights
+ */
+ removeFileHighlights() {
+ const mediaFiles = this.getMediaFileItems();
+ mediaFiles.forEach(fileItem => {
+ fileItem.classList.remove('hovered-file');
+ });
+ }
+}
+
+module.exports = FileListManager; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 | 1x +1x +1x +1x +1x +9x +9x +9x +9x + +1x +1x +1x +1x +1x +2x +2x +2x +2x +2x +2x + +1x +1x +1x +1x +1x +1x +1x +1x + +1x +1x +1x +1x +1x +1x +1x +2x +2x +2x +2x +2x +2x + +1x +1x +1x +1x +1x +1x +1x +1x +1x + +1x +1x +1x +1x +1x +1x +2x +2x +2x +2x +2x + +1x +1x +1x +1x +1x +1x +1x +1x + +1x | /**
+ * ProgressManager - Manages progress display and feedback
+ */
+class ProgressManager {
+ constructor(progressContainer, progressText, progressCount) {
+ this.progressContainer = progressContainer;
+ this.progressText = progressText;
+ this.progressCount = progressCount;
+ }
+
+ /**
+ * Show progress indicator
+ * @param {string} message - Progress message
+ */
+ showProgress(message = 'Scanning directory...') {
+ if (this.progressContainer) {
+ this.progressContainer.style.display = 'block';
+ this.progressText.textContent = message;
+ this.progressCount.textContent = '';
+ }
+ }
+
+ /**
+ * Hide progress indicator
+ */
+ hideProgress() {
+ if (this.progressContainer) {
+ this.progressContainer.style.display = 'none';
+ }
+ }
+
+ /**
+ * Update progress with current/total counts
+ * @param {number} current - Current count
+ * @param {number} total - Total count
+ * @param {string} fileName - Current file name
+ */
+ updateProgress(current, total, fileName) {
+ if (this.progressContainer) {
+ const displayCurrent = current + 1;
+ this.progressText.textContent = `Processing: ${fileName}`;
+ this.progressCount.textContent = `${displayCurrent} of ${total} files`;
+ }
+ }
+
+ /**
+ * Update progress with custom text
+ * @param {string} text - Progress text
+ */
+ updateProgressText(text) {
+ if (this.progressText) {
+ this.progressText.textContent = text;
+ }
+ }
+
+ /**
+ * Update progress count display
+ * @param {number} current - Current count
+ * @param {number} total - Total count
+ */
+ updateProgressCount(current, total) {
+ if (this.progressCount) {
+ const displayCurrent = current + 1;
+ this.progressCount.textContent = `${displayCurrent} of ${total} files`;
+ }
+ }
+
+ /**
+ * Show directory feedback
+ * @param {string} folderName - Folder name
+ */
+ showDirectoryFeedback(folderName) {
+ console.log(`Directory "${folderName}" created and file moved`);
+ }
+}
+
+module.exports = ProgressManager; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| AppState.js | +
+
+ |
+ 98.9% | +180/182 | +100% | +24/24 | +95.45% | +21/22 | +98.9% | +180/182 | +
| EpisodeManager.js | +
+
+ |
+ 81.27% | +204/251 | +85.1% | +40/47 | +84.61% | +11/13 | +81.27% | +204/251 | +
| FileListManager.js | +
+
+ |
+ 80.88% | +275/340 | +78.37% | +29/37 | +70.58% | +12/17 | +80.88% | +275/340 | +
| ProgressManager.js | +
+
+ |
+ 100% | +70/70 | +100% | +7/7 | +100% | +7/7 | +100% | +70/70 | +