Gallery: Add video uploads to gallery (#4796) * Gallery: Add video uploads to gallery * Improve thumbnailing logic * Add error handling to thumnailing process * Improve formatting in utils.js
Signed| @@ -73,7 +73,7 @@ | |||
| 73 | "is-docker": "^3.0.0", | 73 | "is-docker": "^3.0.0", |
| 74 | "localforage": "^1.10.0", | 74 | "localforage": "^1.10.0", |
| 75 | "lodash": "^4.17.21", | 75 | "lodash": "^4.17.21", |
| 76 | "mime-types": "^3.0.1", | 76 | "mime-types": "^3.0.2", |
| 77 | "moment": "^2.30.1", | 77 | "moment": "^2.30.1", |
| 78 | "morphdom": "^2.7.7", | 78 | "morphdom": "^2.7.7", |
| 79 | "multer": "^2.0.2", | 79 | "multer": "^2.0.2", |
| @@ -6432,15 +6432,19 @@ | |||
| 6432 | } | 6432 | } |
| 6433 | }, | 6433 | }, |
| 6434 | "node_modules/mime-types": { | 6434 | "node_modules/mime-types": { |
| 6435 | "version": "3.0.1", | 6435 | "version": "3.0.2", |
| 6436 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", | 6436 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", |
| 6437 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", | 6437 | "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", |
| 6438 | "license": "MIT", | 6438 | "license": "MIT", |
| 6439 | "dependencies": { | 6439 | "dependencies": { |
| 6440 | "mime-db": "^1.54.0" | 6440 | "mime-db": "^1.54.0" |
| 6441 | }, | 6441 | }, |
| 6442 | "engines": { | 6442 | "engines": { |
| 6443 | "node": ">= 0.6" | 6443 | "node": ">=18" |
| 6444 | }, | ||
| 6445 | "funding": { | ||
| 6446 | "type": "opencollective", | ||
| 6447 | "url": "https://opencollective.com/express" | ||
| 6444 | } | 6448 | } |
| 6445 | }, | 6449 | }, |
| 6446 | "node_modules/mime-types/node_modules/mime-db": { | 6450 | "node_modules/mime-types/node_modules/mime-db": { |
| @@ -63,7 +63,7 @@ | |||
| 63 | "is-docker": "^3.0.0", | 63 | "is-docker": "^3.0.0", |
| 64 | "localforage": "^1.10.0", | 64 | "localforage": "^1.10.0", |
| 65 | "lodash": "^4.17.21", | 65 | "lodash": "^4.17.21", |
| 66 | "mime-types": "^3.0.1", | 66 | "mime-types": "^3.0.2", |
| 67 | "moment": "^2.30.1", | 67 | "moment": "^2.30.1", |
| 68 | "morphdom": "^2.7.7", | 68 | "morphdom": "^2.7.7", |
| 69 | "multer": "^2.0.2", | 69 | "multer": "^2.0.2", |
| @@ -164,7 +164,7 @@ | |||
| 164 | "@types/yargs": "^17.0.33", | 164 | "@types/yargs": "^17.0.33", |
| 165 | "@types/yauzl": "^2.10.3", | 165 | "@types/yauzl": "^2.10.3", |
| 166 | "eslint": "^8.57.1", | 166 | "eslint": "^8.57.1", |
| 167 | "eslint-plugin-jsdoc": "^48.10.0", | 167 | "eslint-plugin-jest": "^27.9.0", |
| 168 | "eslint-plugin-jest": "^27.9.0" | 168 | "eslint-plugin-jsdoc": "^48.10.0" |
| 169 | } | 169 | } |
| 170 | } | 170 | } |
| @@ -118,6 +118,17 @@ export const MEDIA_TYPE = { | |||
| 118 | }; | 118 | }; |
| 119 | 119 | ||
| 120 | /** | 120 | /** |
| 121 | * Bitwise flag-style media request types. | ||
| 122 | * @enum {number} | ||
| 123 | * @readonly | ||
| 124 | */ | ||
| 125 | export const MEDIA_REQUEST_TYPE = { | ||
| 126 | IMAGE: 0b001, | ||
| 127 | VIDEO: 0b010, | ||
| 128 | AUDIO: 0b100, | ||
| 129 | }; | ||
| 130 | |||
| 131 | /** | ||
| 121 | * Scroll behavior options when appending media to messages. | 132 | * Scroll behavior options when appending media to messages. |
| 122 | * @enum {string} | 133 | * @enum {string} |
| 123 | * @readonly | 134 | * @readonly |
| @@ -8,7 +8,7 @@ import { | |||
| 8 | animation_easing, | 8 | animation_easing, |
| 9 | } from '../../../script.js'; | 9 | } from '../../../script.js'; |
| 10 | import { groups, selected_group } from '../../group-chats.js'; | 10 | import { groups, selected_group } from '../../group-chats.js'; |
| 11 | import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename, saveBase64AsFile, getFileExtension } from '../../utils.js'; | 11 | import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename, saveBase64AsFile, getFileExtension, getVideoThumbnail, clamp } from '../../utils.js'; |
| 12 | import { loadMovingUIState } from '../../power-user.js'; | 12 | import { loadMovingUIState } from '../../power-user.js'; |
| 13 | import { dragElement } from '../../RossAscends-mods.js'; | 13 | import { dragElement } from '../../RossAscends-mods.js'; |
| 14 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 14 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| @@ -19,17 +19,14 @@ import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnum | |||
| 19 | import { t, translate } from '../../i18n.js'; | 19 | import { t, translate } from '../../i18n.js'; |
| 20 | import { Popup } from '../../popup.js'; | 20 | import { Popup } from '../../popup.js'; |
| 21 | import { deleteMediaFromServer } from '../../chats.js'; | 21 | import { deleteMediaFromServer } from '../../chats.js'; |
| 22 | import { MEDIA_REQUEST_TYPE, VIDEO_EXTENSIONS } from '../../constants.js'; | ||
| 22 | 23 | ||
| 24 | const isVideo = (/** @type {string} */ url) => VIDEO_EXTENSIONS.some(ext => new RegExp(`.${ext}$`, 'i').test(url)); | ||
| 23 | const extensionName = 'gallery'; | 25 | const extensionName = 'gallery'; |
| 24 | const extensionFolderPath = `scripts/extensions/${extensionName}/`; | 26 | const extensionFolderPath = `scripts/extensions/${extensionName}/`; |
| 25 | let firstTime = true; | 27 | let firstTime = true; |
| 26 | let deleteModeActive = false; | 28 | let deleteModeActive = false; |
| 27 | 29 | ||
| 28 | // Exposed defaults for future tweaking | ||
| 29 | let thumbnailHeight = 150; | ||
| 30 | let paginationVisiblePages = 10; | ||
| 31 | let paginationMaxLinesPerPage = 2; | ||
| 32 | let galleryMaxRows = 3; | ||
| 33 | 30 | ||
| 34 | // Remove all draggables associated with the gallery | 31 | // Remove all draggables associated with the gallery |
| 35 | $('#movingDivs').on('click', '.dragClose', function () { | 32 | $('#movingDivs').on('click', '.dragClose', function () { |
| @@ -122,17 +119,34 @@ async function getGalleryItems(url) { | |||
| 122 | folder: url, | 119 | folder: url, |
| 123 | sortField: sortObj.field, | 120 | sortField: sortObj.field, |
| 124 | sortOrder: sortObj.order, | 121 | sortOrder: sortObj.order, |
| 122 | type: MEDIA_REQUEST_TYPE.IMAGE | MEDIA_REQUEST_TYPE.VIDEO, | ||
| 125 | }), | 123 | }), |
| 126 | }); | 124 | }); |
| 127 | 125 | ||
| 128 | url = await getSanitizedFilename(url); | 126 | url = await getSanitizedFilename(url); |
| 129 | 127 | ||
| 130 | const data = await response.json(); | 128 | const data = await response.json(); |
| 131 | const items = data.map((file) => ({ | 129 | const items = []; |
| 130 | |||
| 131 | for (const file of data) { | ||
| 132 | const item = { | ||
| 132 | src: `user/images/${url}/${file}`, | 133 | src: `user/images/${url}/${file}`, |
| 133 | srct: `user/images/${url}/${file}`, | 134 | srct: `user/images/${url}/${file}`, |
| 134 | title: '', // Optional title for each item | 135 | title: '', // Optional title for each item |
| 135 | })); | 136 | }; |
| 137 | |||
| 138 | if (isVideo(file)) { | ||
| 139 | try { | ||
| 140 | // 150px of max height with some allowance for various aspect ratios | ||
| 141 | const maxSide = Math.round(150 * 1.5); | ||
| 142 | item.srct = await getVideoThumbnail(item.src, maxSide, maxSide); | ||
| 143 | } catch (error) { | ||
| 144 | console.error('Failed to generate video thumbnail for gallery:', error); | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | items.push(item); | ||
| 149 | } | ||
| 136 | 150 | ||
| 137 | return items; | 151 | return items; |
| 138 | } | 152 | } |
| @@ -198,6 +212,12 @@ function getSortOrder() { | |||
| 198 | * @returns {Promise<void>} - Promise representing the completion of the gallery initialization. | 212 | * @returns {Promise<void>} - Promise representing the completion of the gallery initialization. |
| 199 | */ | 213 | */ |
| 200 | async function initGallery(items, url) { | 214 | async function initGallery(items, url) { |
| 215 | // Exposed defaults for future tweaking | ||
| 216 | const thumbnailHeight = 150; | ||
| 217 | const paginationVisiblePages = 5; | ||
| 218 | const paginationMaxLinesPerPage = 2; | ||
| 219 | const galleryMaxRows = clamp(Math.floor((window.innerHeight * 0.9 - 75) / thumbnailHeight), 1, 10); | ||
| 220 | |||
| 201 | const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`; | 221 | const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`; |
| 202 | const gallery = $('#dragGallery'); | 222 | const gallery = $('#dragGallery'); |
| 203 | gallery.addClass(nonce); | 223 | gallery.addClass(nonce); |
| @@ -210,6 +230,7 @@ async function initGallery(items, url) { | |||
| 210 | galleryMaxRows: galleryMaxRows, | 230 | galleryMaxRows: galleryMaxRows, |
| 211 | galleryPaginationTopButtons: false, | 231 | galleryPaginationTopButtons: false, |
| 212 | galleryNavigationOverlayButtons: true, | 232 | galleryNavigationOverlayButtons: true, |
| 233 | galleryPaginationMode: 'rectangles', | ||
| 213 | galleryTheme: { | 234 | galleryTheme: { |
| 214 | navigationBar: { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' }, | 235 | navigationBar: { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' }, |
| 215 | navigationBreadcrumb: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' }, | 236 | navigationBreadcrumb: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' }, |
| @@ -399,7 +420,7 @@ async function makeMovable(url) { | |||
| 399 | // Create a hidden file input | 420 | // Create a hidden file input |
| 400 | const fileInput = document.createElement('input'); | 421 | const fileInput = document.createElement('input'); |
| 401 | fileInput.type = 'file'; | 422 | fileInput.type = 'file'; |
| 402 | fileInput.accept = 'image/*'; | 423 | fileInput.accept = 'image/*,video/*'; |
| 403 | fileInput.multiple = true; | 424 | fileInput.multiple = true; |
| 404 | fileInput.style.display = 'none'; | 425 | fileInput.style.display = 'none'; |
| 405 | 426 | ||
| @@ -617,12 +638,19 @@ function makeDragImg(id, url) { | |||
| 617 | const newElement = document.importNode(template.content, true); | 638 | const newElement = document.importNode(template.content, true); |
| 618 | 639 | ||
| 619 | // Step 2: Append the given image | 640 | // Step 2: Append the given image |
| 620 | const imgElem = document.createElement('img'); | 641 | const mediaElement = isVideo(url) |
| 621 | imgElem.src = url; | 642 | ? document.createElement('video') |
| 643 | : document.createElement('img'); | ||
| 644 | mediaElement.src = url; | ||
| 645 | if (mediaElement instanceof HTMLVideoElement) { | ||
| 646 | mediaElement.controls = true; | ||
| 647 | mediaElement.autoplay = true; | ||
| 648 | } | ||
| 649 | |||
| 622 | let uniqueId = `draggable_${id}`; | 650 | let uniqueId = `draggable_${id}`; |
| 623 | const draggableElem = /** @type {HTMLElement} */ (newElement.querySelector('.draggable')); | 651 | const draggableElem = /** @type {HTMLElement} */ (newElement.querySelector('.draggable')); |
| 624 | if (draggableElem) { | 652 | if (draggableElem) { |
| 625 | draggableElem.appendChild(imgElem); | 653 | draggableElem.appendChild(mediaElement); |
| 626 | 654 | ||
| 627 | // Find a unique id for the draggable element | 655 | // Find a unique id for the draggable element |
| 628 | 656 | ||
| @@ -49,3 +49,7 @@ | |||
| 49 | opacity: 1; | 49 | opacity: 1; |
| 50 | filter: unset; | 50 | filter: unset; |
| 51 | } | 51 | } |
| 52 | |||
| 53 | .galleryImageDraggable video { | ||
| 54 | width: 100%; | ||
| 55 | } | ||
| @@ -1207,6 +1207,84 @@ export function getVideoDurationFromDataURL(dataUrl) { | |||
| 1207 | } | 1207 | } |
| 1208 | 1208 | ||
| 1209 | /** | 1209 | /** |
| 1210 | * Gets a thumbnail image from a video URL. | ||
| 1211 | * @param {string} videoUrl URL of the video | ||
| 1212 | * @param {number|null} [maxWidth=null] Maximum width of the thumbnail | ||
| 1213 | * @param {number|null} [maxHeight=null] Maximum height of the thumbnail | ||
| 1214 | * @param {string} [type='image/jpeg'] MIME type of the thumbnail | ||
| 1215 | * @returns {Promise<string>} Promise that resolves to a data URL of the video thumbnail | ||
| 1216 | */ | ||
| 1217 | export function getVideoThumbnail(videoUrl, maxWidth = null, maxHeight = null, type = 'image/jpeg') { | ||
| 1218 | const video = document.createElement('video'); | ||
| 1219 | video.src = videoUrl; | ||
| 1220 | return new Promise((resolve, reject) => { | ||
| 1221 | video.onloadeddata = function () { | ||
| 1222 | // Set the time to capture the thumbnail at the middle of the video | ||
| 1223 | video.currentTime = video.duration / 2; | ||
| 1224 | }; | ||
| 1225 | video.onseeked = function () { | ||
| 1226 | // Create a canvas to draw the thumbnail | ||
| 1227 | const canvas = document.createElement('canvas'); | ||
| 1228 | const ctx = canvas.getContext('2d'); | ||
| 1229 | const { thumbnailWidth, thumbnailHeight } = calculateThumbnailSize(video.videoWidth, video.videoHeight, maxWidth, maxHeight); | ||
| 1230 | |||
| 1231 | canvas.width = thumbnailWidth; | ||
| 1232 | canvas.height = thumbnailHeight; | ||
| 1233 | ctx.imageSmoothingEnabled = true; | ||
| 1234 | ctx.imageSmoothingQuality = 'high'; | ||
| 1235 | ctx.fillStyle = 'black'; | ||
| 1236 | ctx.fillRect(0, 0, thumbnailWidth, thumbnailHeight); | ||
| 1237 | ctx.drawImage(video, 0, 0, thumbnailWidth, thumbnailHeight); | ||
| 1238 | // Get the data URL of the thumbnail | ||
| 1239 | const dataUrl = canvas.toDataURL(type); | ||
| 1240 | resolve(dataUrl); | ||
| 1241 | }; | ||
| 1242 | video.onerror = function () { | ||
| 1243 | reject(new Error('Failed to load video')); | ||
| 1244 | }; | ||
| 1245 | }); | ||
| 1246 | } | ||
| 1247 | |||
| 1248 | /** | ||
| 1249 | * Calculates the thumbnail size for a media element while maintaining aspect ratio. | ||
| 1250 | * @param {number} width Media width | ||
| 1251 | * @param {number} height Media height | ||
| 1252 | * @param {number?} maxWidth Max width (null = no limit) | ||
| 1253 | * @param {number?} maxHeight Max height (null = no limit) | ||
| 1254 | * @returns {{ thumbnailWidth: number, thumbnailHeight: number }} Thumbnail size | ||
| 1255 | */ | ||
| 1256 | export function calculateThumbnailSize(width, height, maxWidth, maxHeight) { | ||
| 1257 | // Calculate the thumbnail dimensions while maintaining the aspect ratio | ||
| 1258 | const aspectRatio = width / height; | ||
| 1259 | let thumbnailWidth = maxWidth; | ||
| 1260 | let thumbnailHeight = maxHeight; | ||
| 1261 | |||
| 1262 | if (maxWidth === null) { | ||
| 1263 | thumbnailWidth = width; | ||
| 1264 | maxWidth = width; | ||
| 1265 | } | ||
| 1266 | |||
| 1267 | if (maxHeight === null) { | ||
| 1268 | thumbnailHeight = height; | ||
| 1269 | maxHeight = height; | ||
| 1270 | } | ||
| 1271 | |||
| 1272 | // Do not upscale if image is already smaller than max dimensions | ||
| 1273 | if (width <= maxWidth && height <= maxHeight) { | ||
| 1274 | thumbnailWidth = width; | ||
| 1275 | thumbnailHeight = height; | ||
| 1276 | } else { | ||
| 1277 | if (width > height) { | ||
| 1278 | thumbnailHeight = maxWidth / aspectRatio; | ||
| 1279 | } else { | ||
| 1280 | thumbnailWidth = maxHeight * aspectRatio; | ||
| 1281 | } | ||
| 1282 | } | ||
| 1283 | |||
| 1284 | return { thumbnailWidth: Math.round(thumbnailWidth), thumbnailHeight: Math.round(thumbnailHeight) }; | ||
| 1285 | } | ||
| 1286 | |||
| 1287 | /** | ||
| 1210 | * Gets the duration of an audio from a data URL. | 1288 | * Gets the duration of an audio from a data URL. |
| 1211 | * @param {string} dataUrl Audio data URL | 1289 | * @param {string} dataUrl Audio data URL |
| 1212 | * @returns {Promise<number>} Duration in seconds | 1290 | * @returns {Promise<number>} Duration in seconds |
| @@ -1690,33 +1768,7 @@ export function createThumbnail(dataUrl, maxWidth = null, maxHeight = null, type | |||
| 1690 | img.onload = () => { | 1768 | img.onload = () => { |
| 1691 | const canvas = document.createElement('canvas'); | 1769 | const canvas = document.createElement('canvas'); |
| 1692 | const ctx = canvas.getContext('2d'); | 1770 | const ctx = canvas.getContext('2d'); |
| 1693 | 1771 | const { thumbnailWidth, thumbnailHeight } = calculateThumbnailSize(img.width, img.height, maxWidth, maxHeight); | |
| 1694 | // Calculate the thumbnail dimensions while maintaining the aspect ratio | ||
| 1695 | const aspectRatio = img.width / img.height; | ||
| 1696 | let thumbnailWidth = maxWidth; | ||
| 1697 | let thumbnailHeight = maxHeight; | ||
| 1698 | |||
| 1699 | if (maxWidth === null) { | ||
| 1700 | thumbnailWidth = img.width; | ||
| 1701 | maxWidth = img.width; | ||
| 1702 | } | ||
| 1703 | |||
| 1704 | if (maxHeight === null) { | ||
| 1705 | thumbnailHeight = img.height; | ||
| 1706 | maxHeight = img.height; | ||
| 1707 | } | ||
| 1708 | |||
| 1709 | // Do not upscale if image is already smaller than max dimensions | ||
| 1710 | if (img.width <= maxWidth && img.height <= maxHeight) { | ||
| 1711 | thumbnailWidth = img.width; | ||
| 1712 | thumbnailHeight = img.height; | ||
| 1713 | } else { | ||
| 1714 | if (img.width > img.height) { | ||
| 1715 | thumbnailHeight = maxWidth / aspectRatio; | ||
| 1716 | } else { | ||
| 1717 | thumbnailWidth = maxHeight * aspectRatio; | ||
| 1718 | } | ||
| 1719 | } | ||
| 1720 | 1772 | ||
| 1721 | // Set the canvas dimensions and draw the resized image | 1773 | // Set the canvas dimensions and draw the resized image |
| 1722 | canvas.width = thumbnailWidth; | 1774 | canvas.width = thumbnailWidth; |
| @@ -2386,6 +2438,7 @@ export async function fetchFaFile(name) { | |||
| 2386 | .map(rule => rule.selectorText.split(/,\s*/).map(selector => selector.split('::').shift().slice(1))) | 2438 | .map(rule => rule.selectorText.split(/,\s*/).map(selector => selector.split('::').shift().slice(1))) |
| 2387 | ; | 2439 | ; |
| 2388 | } | 2440 | } |
| 2441 | |||
| 2389 | export async function fetchFa() { | 2442 | export async function fetchFa() { |
| 2390 | return [...new Set((await Promise.all([ | 2443 | return [...new Set((await Promise.all([ |
| 2391 | fetchFaFile('fontawesome.min.css'), | 2444 | fetchFaFile('fontawesome.min.css'), |
| @@ -2782,4 +2835,5 @@ export async function importFromExternalUrl(url, { preserveFileName = null } = { | |||
| 2782 | break; | 2835 | break; |
| 2783 | } | 2836 | } |
| 2784 | } | 2837 | } |
| 2838 | |||
| 2785 | export const clamp = (value, min, max) => Math.min(Math.max(value, min), max); | 2839 | export const clamp = (value, min, max) => Math.min(Math.max(value, min), max); |
| @@ -513,6 +513,16 @@ export const MEDIA_EXTENSIONS = [ | |||
| 513 | 'aiff', | 513 | 'aiff', |
| 514 | ]; | 514 | ]; |
| 515 | 515 | ||
| 516 | /** | ||
| 517 | * Bitwise flag-style media request types. | ||
| 518 | */ | ||
| 519 | export const MEDIA_REQUEST_TYPE = { | ||
| 520 | IMAGE: 0b001, | ||
| 521 | VIDEO: 0b010, | ||
| 522 | AUDIO: 0b100, | ||
| 523 | }; | ||
| 524 | |||
| 525 | |||
| 516 | export const ZAI_ENDPOINT = { | 526 | export const ZAI_ENDPOINT = { |
| 517 | COMMON: 'common', | 527 | COMMON: 'common', |
| 518 | CODING: 'coding', | 528 | CODING: 'coding', |
| @@ -6,7 +6,7 @@ import express from 'express'; | |||
| 6 | import sanitize from 'sanitize-filename'; | 6 | import sanitize from 'sanitize-filename'; |
| 7 | 7 | ||
| 8 | import { clientRelativePath, removeFileExtension, getImages, isPathUnderParent } from '../util.js'; | 8 | import { clientRelativePath, removeFileExtension, getImages, isPathUnderParent } from '../util.js'; |
| 9 | import { MEDIA_EXTENSIONS } from '../constants.js'; | 9 | import { MEDIA_EXTENSIONS, MEDIA_REQUEST_TYPE } from '../constants.js'; |
| 10 | 10 | ||
| 11 | /** | 11 | /** |
| 12 | * Ensure the directory for the provided file path exists. | 12 | * Ensure the directory for the provided file path exists. |
| @@ -93,6 +93,7 @@ router.post('/list/:folder?', (request, response) => { | |||
| 93 | } | 93 | } |
| 94 | 94 | ||
| 95 | const directoryPath = path.join(request.user.directories.userImages, sanitize(request.body.folder)); | 95 | const directoryPath = path.join(request.user.directories.userImages, sanitize(request.body.folder)); |
| 96 | const type = Number(request.body.type ?? MEDIA_REQUEST_TYPE.IMAGE); | ||
| 96 | const sort = request.body.sortField || 'date'; | 97 | const sort = request.body.sortField || 'date'; |
| 97 | const order = request.body.sortOrder || 'asc'; | 98 | const order = request.body.sortOrder || 'asc'; |
| 98 | 99 | ||
| @@ -100,7 +101,7 @@ router.post('/list/:folder?', (request, response) => { | |||
| 100 | fs.mkdirSync(directoryPath, { recursive: true }); | 101 | fs.mkdirSync(directoryPath, { recursive: true }); |
| 101 | } | 102 | } |
| 102 | 103 | ||
| 103 | const images = getImages(directoryPath, sort); | 104 | const images = getImages(directoryPath, sort, type); |
| 104 | if (order === 'desc') { | 105 | if (order === 'desc') { |
| 105 | images.reverse(); | 106 | images.reverse(); |
| 106 | } | 107 | } |
| @@ -17,7 +17,7 @@ import mime from 'mime-types'; | |||
| 17 | import { default as simpleGit } from 'simple-git'; | 17 | import { default as simpleGit } from 'simple-git'; |
| 18 | import chalk from 'chalk'; | 18 | import chalk from 'chalk'; |
| 19 | import bytes from 'bytes'; | 19 | import bytes from 'bytes'; |
| 20 | import { LOG_LEVELS, CHAT_COMPLETION_SOURCES } from './constants.js'; | 20 | import { LOG_LEVELS, CHAT_COMPLETION_SOURCES, MEDIA_REQUEST_TYPE } from './constants.js'; |
| 21 | import { serverDirectory } from './server-directory.js'; | 21 | import { serverDirectory } from './server-directory.js'; |
| 22 | import { isFirefox } from './express-common.js'; | 22 | import { isFirefox } from './express-common.js'; |
| 23 | 23 | ||
| @@ -500,9 +500,10 @@ export function removeOldBackups(directory, prefix, limit = null) { | |||
| 500 | * Get a list of images in a directory. | 500 | * Get a list of images in a directory. |
| 501 | * @param {string} directoryPath Path to the directory containing the images | 501 | * @param {string} directoryPath Path to the directory containing the images |
| 502 | * @param {'name' | 'date'} sortBy Sort images by name or date | 502 | * @param {'name' | 'date'} sortBy Sort images by name or date |
| 503 | * @param {number} type Bitwise flag representing media types to include | ||
| 503 | * @returns {string[]} List of image file names | 504 | * @returns {string[]} List of image file names |
| 504 | */ | 505 | */ |
| 505 | export function getImages(directoryPath, sortBy = 'name') { | 506 | export function getImages(directoryPath, sortBy = 'name', type = MEDIA_REQUEST_TYPE.IMAGE) { |
| 506 | function getSortFunction() { | 507 | function getSortFunction() { |
| 507 | switch (sortBy) { | 508 | switch (sortBy) { |
| 508 | case 'name': | 509 | case 'name': |
| @@ -515,10 +516,24 @@ export function getImages(directoryPath, sortBy = 'name') { | |||
| 515 | } | 516 | } |
| 516 | 517 | ||
| 517 | return fs | 518 | return fs |
| 518 | .readdirSync(directoryPath) | 519 | .readdirSync(directoryPath, { withFileTypes: true }) |
| 520 | .filter(dirent => dirent.isFile()) | ||
| 521 | .map(dirent => dirent.name) | ||
| 519 | .filter(file => { | 522 | .filter(file => { |
| 520 | const type = mime.lookup(file); | 523 | const fileType = mime.lookup(file); |
| 521 | return type && type.startsWith('image/'); | 524 | if (!fileType) { |
| 525 | return false; | ||
| 526 | } | ||
| 527 | if ((type & MEDIA_REQUEST_TYPE.IMAGE) && fileType.startsWith('image/')) { | ||
| 528 | return true; | ||
| 529 | } | ||
| 530 | if ((type & MEDIA_REQUEST_TYPE.VIDEO) && fileType.startsWith('video/')) { | ||
| 531 | return true; | ||
| 532 | } | ||
| 533 | if ((type & MEDIA_REQUEST_TYPE.AUDIO) && fileType.startsWith('audio/')) { | ||
| 534 | return true; | ||
| 535 | } | ||
| 536 | return false; | ||
| 522 | }) | 537 | }) |
| 523 | .sort(getSortFunction()); | 538 | .sort(getSortFunction()); |
| 524 | } | 539 | } |