fix(auth+bugs): optional auth bypass, login routes, conform column name, panel metadata fields, login page: main.js
This commit is contained in:
parent
47c113e6c3
commit
72c4a7f136
1 changed files with 64 additions and 86 deletions
|
|
@ -11,20 +11,20 @@ const csInterface = new CSInterface();
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
serverUrl: localStorage.getItem('mam_server_url') || 'http://localhost:7434',
|
serverUrl: localStorage.getItem('mam_server_url') || 'http://localhost:7434',
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
selectedAsset: null,
|
selectedAsset: null,
|
||||||
assets: [],
|
assets: [],
|
||||||
projects: [],
|
projects: [],
|
||||||
selectedProject: 'all',
|
selectedProject: 'all',
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
totalAssets: 0,
|
totalAssets: 0,
|
||||||
downloadProgress: 0,
|
downloadProgress: 0,
|
||||||
isDownloading: false,
|
isDownloading: false,
|
||||||
thumbCache: {}, // assetId -> signed URL
|
thumbCache: {}, // assetId -> signed URL
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -65,7 +65,7 @@ function initDOMElements() {
|
||||||
const thumbObserver = new IntersectionObserver((entries) => {
|
const thumbObserver = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
const img = entry.target;
|
const img = entry.target;
|
||||||
const assetId = img.dataset.assetId;
|
const assetId = img.dataset.assetId;
|
||||||
if (assetId && !img.dataset.loaded) loadThumbnail(img, assetId);
|
if (assetId && !img.dataset.loaded) loadThumbnail(img, assetId);
|
||||||
thumbObserver.unobserve(img);
|
thumbObserver.unobserve(img);
|
||||||
|
|
@ -90,7 +90,7 @@ async function loadThumbnail(img, assetId) {
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.dataset.loaded = '1';
|
img.dataset.loaded = '1';
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// thumbnail unavailable — leave placeholder visible
|
// thumbnail unavailable — leave placeholder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,9 +109,8 @@ function setupEventListeners() {
|
||||||
elements.serverUrlInput.addEventListener('change', (e) => {
|
elements.serverUrlInput.addEventListener('change', (e) => {
|
||||||
state.serverUrl = e.target.value.trim().replace(/\/$/, '');
|
state.serverUrl = e.target.value.trim().replace(/\/$/, '');
|
||||||
localStorage.setItem('mam_server_url', state.serverUrl);
|
localStorage.setItem('mam_server_url', state.serverUrl);
|
||||||
state.thumbCache = {}; // bust cache when server changes
|
state.thumbCache = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.connectBtn.addEventListener('click', connectToServer);
|
elements.connectBtn.addEventListener('click', connectToServer);
|
||||||
elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
|
elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
|
||||||
elements.projectFilter.addEventListener('change', handleProjectFilter);
|
elements.projectFilter.addEventListener('change', handleProjectFilter);
|
||||||
|
|
@ -136,10 +135,11 @@ async function connectToServer() {
|
||||||
elements.connectBtn.disabled = true;
|
elements.connectBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the projects endpoint as a health check (no separate /health route exists)
|
// Use the projects endpoint as a connectivity check
|
||||||
const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
|
const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
@ -148,7 +148,6 @@ async function connectToServer() {
|
||||||
elements.connectBtn.textContent = 'Reconnect';
|
elements.connectBtn.textContent = 'Reconnect';
|
||||||
logMessage('Connected to Wild Dragon MAM');
|
logMessage('Connected to Wild Dragon MAM');
|
||||||
|
|
||||||
// Pass the already-fetched JSON to avoid a second round-trip
|
|
||||||
const projectData = await response.json();
|
const projectData = await response.json();
|
||||||
await fetchProjects(projectData);
|
await fetchProjects(projectData);
|
||||||
await fetchAssets();
|
await fetchAssets();
|
||||||
|
|
@ -170,7 +169,7 @@ async function connectToServer() {
|
||||||
function updateConnectionStatus(status) {
|
function updateConnectionStatus(status) {
|
||||||
const indicator = elements.statusIndicator;
|
const indicator = elements.statusIndicator;
|
||||||
indicator.classList.remove('connected', 'connecting');
|
indicator.classList.remove('connected', 'connecting');
|
||||||
if (status === 'connected') indicator.classList.add('connected');
|
if (status === 'connected') indicator.classList.add('connected');
|
||||||
else if (status === 'connecting') indicator.classList.add('connecting');
|
else if (status === 'connecting') indicator.classList.add('connecting');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,19 +177,15 @@ function updateConnectionStatus(status) {
|
||||||
// API Calls
|
// API Calls
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Load projects into state and the filter dropdown.
|
|
||||||
* @param {Array} [preloadedData] - If provided, skips the fetch (reuse connection-check response).
|
|
||||||
*/
|
|
||||||
async function fetchProjects(preloadedData) {
|
async function fetchProjects(preloadedData) {
|
||||||
try {
|
try {
|
||||||
let projects;
|
let projects;
|
||||||
if (preloadedData) {
|
if (preloadedData) {
|
||||||
// GET /api/v1/projects returns a plain array (not { projects: [] })
|
|
||||||
projects = Array.isArray(preloadedData) ? preloadedData : [];
|
projects = Array.isArray(preloadedData) ? preloadedData : [];
|
||||||
} else {
|
} else {
|
||||||
const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
|
const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -203,7 +198,7 @@ async function fetchProjects(preloadedData) {
|
||||||
elements.projectFilter.innerHTML = '<option value="all">All Projects</option>';
|
elements.projectFilter.innerHTML = '<option value="all">All Projects</option>';
|
||||||
state.projects.forEach((p) => {
|
state.projects.forEach((p) => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = p.id;
|
opt.value = p.id;
|
||||||
opt.textContent = p.name;
|
opt.textContent = p.name;
|
||||||
elements.projectFilter.appendChild(opt);
|
elements.projectFilter.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
@ -217,24 +212,21 @@ async function fetchProjects(preloadedData) {
|
||||||
|
|
||||||
async function fetchAssets(page = 0) {
|
async function fetchAssets(page = 0) {
|
||||||
if (!state.isConnected) return;
|
if (!state.isConnected) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
offset: page * state.pageSize,
|
offset: page * state.pageSize,
|
||||||
limit: state.pageSize,
|
limit: state.pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state.searchQuery) params.append('search', state.searchQuery);
|
if (state.searchQuery) params.append('search', state.searchQuery);
|
||||||
if (state.selectedProject !== 'all') params.append('project_id', state.selectedProject);
|
if (state.selectedProject !== 'all') params.append('project_id', state.selectedProject);
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${state.serverUrl}/api/v1/assets?${params.toString()}`,
|
`${state.serverUrl}/api/v1/assets?${params.toString()}`,
|
||||||
{ headers: { Accept: 'application/json' } }
|
{ headers: { Accept: 'application/json' }, credentials: 'include' }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
state.assets = data.assets || [];
|
state.assets = data.assets || [];
|
||||||
state.totalAssets = data.total || 0;
|
state.totalAssets = data.total || 0;
|
||||||
state.currentPage = page;
|
state.currentPage = page;
|
||||||
|
|
@ -252,6 +244,7 @@ async function fetchAssetDetails(assetId) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, {
|
const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, {
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
|
@ -263,11 +256,12 @@ async function fetchAssetDetails(assetId) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a short-lived presigned URL for the H.264 proxy of the given asset.
|
* Returns a short-lived presigned URL for the H.264 proxy of the given asset.
|
||||||
* GET /api/v1/assets/:id/stream -> { url: '...' }
|
* GET /api/v1/assets/:id/stream -> { url: '...' }
|
||||||
*/
|
*/
|
||||||
async function getSignedDownloadUrl(assetId) {
|
async function getSignedDownloadUrl(assetId) {
|
||||||
const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
|
const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
|
if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
|
||||||
const { url } = await response.json();
|
const { url } = await response.json();
|
||||||
|
|
@ -301,44 +295,44 @@ function createAssetCard(asset) {
|
||||||
}
|
}
|
||||||
card.dataset.assetId = asset.id;
|
card.dataset.assetId = asset.id;
|
||||||
|
|
||||||
// Thumbnail — lazy loaded by IntersectionObserver
|
// Thumbnail — lazy loaded via IntersectionObserver
|
||||||
const thumbnail = document.createElement('div');
|
const thumbnail = document.createElement('div');
|
||||||
thumbnail.className = 'asset-thumbnail';
|
thumbnail.className = 'asset-thumbnail';
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.dataset.assetId = asset.id;
|
img.dataset.assetId = asset.id;
|
||||||
img.alt = escapeHtml(asset.display_name || asset.filename || '');
|
img.alt = escapeHtml(asset.display_name || asset.filename || '');
|
||||||
img.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
|
img.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
|
||||||
img.onerror = () => { img.style.display = 'none'; };
|
img.onerror = () => { img.style.display = 'none'; };
|
||||||
thumbnail.appendChild(img);
|
thumbnail.appendChild(img);
|
||||||
thumbObserver.observe(img);
|
thumbObserver.observe(img);
|
||||||
|
|
||||||
card.appendChild(thumbnail);
|
card.appendChild(thumbnail);
|
||||||
|
|
||||||
// Info row
|
// Info row
|
||||||
const info = document.createElement('div');
|
const info = document.createElement('div');
|
||||||
info.className = 'asset-info';
|
info.className = 'asset-info';
|
||||||
|
|
||||||
const name = asset.display_name || asset.filename || 'Untitled';
|
const name = asset.display_name || asset.filename || 'Untitled';
|
||||||
const filenameEl = document.createElement('div');
|
const filenameEl = document.createElement('div');
|
||||||
filenameEl.className = 'asset-filename';
|
filenameEl.className = 'asset-filename';
|
||||||
filenameEl.title = name;
|
filenameEl.title = name;
|
||||||
filenameEl.textContent = name;
|
filenameEl.textContent = name;
|
||||||
info.appendChild(filenameEl);
|
info.appendChild(filenameEl);
|
||||||
|
|
||||||
const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
|
const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
|
||||||
const codec = (asset.metadata && asset.metadata.codec) || asset.media_type || 'video';
|
// codec and media_type are top-level columns on the asset record
|
||||||
const meta = document.createElement('div');
|
const codec = asset.codec || asset.media_type || 'video';
|
||||||
meta.className = 'asset-meta';
|
const meta = document.createElement('div');
|
||||||
meta.innerHTML = [
|
meta.className = 'asset-meta';
|
||||||
|
meta.innerHTML = [
|
||||||
'<span>' + (durationSec ? formatDuration(durationSec) : 'N/A') + '</span>',
|
'<span>' + (durationSec ? formatDuration(durationSec) : 'N/A') + '</span>',
|
||||||
'<span>' + escapeHtml(codec.toUpperCase()) + '</span>',
|
'<span>' + escapeHtml(codec.toUpperCase()) + '</span>',
|
||||||
].join('');
|
].join('');
|
||||||
info.appendChild(meta);
|
info.appendChild(meta);
|
||||||
|
|
||||||
const statusStr = asset.status || 'ready';
|
const statusStr = asset.status || 'ready';
|
||||||
const statusBadge = document.createElement('div');
|
const statusBadge = document.createElement('div');
|
||||||
statusBadge.className = 'asset-status-badge status-badge ' + statusStr;
|
statusBadge.className = 'asset-status-badge status-badge ' + statusStr;
|
||||||
statusBadge.textContent = statusStr.toUpperCase();
|
statusBadge.textContent = statusStr.toUpperCase();
|
||||||
info.appendChild(statusBadge);
|
info.appendChild(statusBadge);
|
||||||
|
|
||||||
|
|
@ -350,24 +344,24 @@ function showAssetDetails(asset) {
|
||||||
state.selectedAsset = asset;
|
state.selectedAsset = asset;
|
||||||
elements.detailsPanel.classList.remove('hidden');
|
elements.detailsPanel.classList.remove('hidden');
|
||||||
|
|
||||||
const meta = asset.metadata || {};
|
// All of these are top-level columns in the assets table
|
||||||
elements.detailsFilename.textContent = asset.display_name || asset.filename;
|
elements.detailsFilename.textContent = asset.display_name || asset.filename;
|
||||||
elements.detailsCodec.textContent = meta.codec || asset.media_type || 'Unknown';
|
elements.detailsCodec.textContent = asset.codec || asset.media_type || 'Unknown';
|
||||||
elements.detailsResolution.textContent = meta.resolution || 'N/A';
|
elements.detailsResolution.textContent = asset.resolution || 'N/A';
|
||||||
elements.detailsFps.textContent = meta.fps ? meta.fps + ' fps' : 'N/A';
|
elements.detailsFps.textContent = asset.fps ? asset.fps + ' fps' : 'N/A';
|
||||||
elements.detailsDuration.textContent = asset.duration_ms
|
elements.detailsDuration.textContent = asset.duration_ms
|
||||||
? formatDuration(asset.duration_ms / 1000)
|
? formatDuration(asset.duration_ms / 1000)
|
||||||
: 'N/A';
|
: 'N/A';
|
||||||
elements.detailsSize.textContent = meta.file_size
|
elements.detailsSize.textContent = asset.file_size
|
||||||
? formatFileSize(meta.file_size)
|
? formatFileSize(asset.file_size)
|
||||||
: 'N/A';
|
: 'N/A';
|
||||||
|
|
||||||
elements.detailsTags.innerHTML = '';
|
elements.detailsTags.innerHTML = '';
|
||||||
const tags = asset.tags || [];
|
const tags = asset.tags || [];
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
tags.forEach((tag) => {
|
tags.forEach((tag) => {
|
||||||
const el = document.createElement('span');
|
const el = document.createElement('span');
|
||||||
el.className = 'tag';
|
el.className = 'tag';
|
||||||
el.textContent = tag;
|
el.textContent = tag;
|
||||||
elements.detailsTags.appendChild(el);
|
elements.detailsTags.appendChild(el);
|
||||||
});
|
});
|
||||||
|
|
@ -397,7 +391,7 @@ function handleSearch(e) {
|
||||||
|
|
||||||
function handleProjectFilter(e) {
|
function handleProjectFilter(e) {
|
||||||
state.selectedProject = e.target.value;
|
state.selectedProject = e.target.value;
|
||||||
state.currentPage = 0;
|
state.currentPage = 0;
|
||||||
fetchAssets();
|
fetchAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -428,18 +422,18 @@ async function importAsset(asset) {
|
||||||
try {
|
try {
|
||||||
elements.importBtn.disabled = true;
|
elements.importBtn.disabled = true;
|
||||||
|
|
||||||
// Step 1: get a presigned proxy URL from the MAM API
|
// 1. Get a presigned proxy URL
|
||||||
showProgress('Getting download link...', 5);
|
showProgress('Getting download link...', 5);
|
||||||
const url = await getSignedDownloadUrl(asset.id);
|
const url = await getSignedDownloadUrl(asset.id);
|
||||||
|
|
||||||
// Step 2: download the proxy (H.264) to the OS temp directory
|
// 2. Download proxy to OS temp dir via Node.js
|
||||||
const safeName = sanitizeFilename(
|
const safeName = sanitizeFilename(
|
||||||
(asset.display_name || asset.filename || asset.id) + '.mp4'
|
(asset.display_name || asset.filename || asset.id) + '.mp4'
|
||||||
);
|
);
|
||||||
showProgress('Downloading ' + safeName + '...', 10);
|
showProgress('Downloading ' + safeName + '...', 10);
|
||||||
const filePath = await downloadFile(url, safeName);
|
const filePath = await downloadFile(url, safeName);
|
||||||
|
|
||||||
// Step 3: hand the local path to Premiere Pro via ExtendScript
|
// 3. Hand the local path to Premiere Pro
|
||||||
showProgress('Importing into Premiere Pro...', 85);
|
showProgress('Importing into Premiere Pro...', 85);
|
||||||
await importFileToPremiereProject(filePath);
|
await importFileToPremiereProject(filePath);
|
||||||
|
|
||||||
|
|
@ -456,22 +450,16 @@ async function importAsset(asset) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a remote URL to a local temp file using Node.js http/https.
|
* Downloads a remote URL to a local temp file using Node.js http/https.
|
||||||
*
|
* Requires --enable-nodejs in the CEP manifest.
|
||||||
* Node.js is available via require() in CEP when the manifest contains:
|
|
||||||
* <CEFCommandLine><Parameter>--enable-nodejs</Parameter></CEFCommandLine>
|
|
||||||
*
|
|
||||||
* @param {string} url - Presigned download URL (http or https)
|
|
||||||
* @param {string} filename - Desired local filename (sanitized)
|
|
||||||
* @returns {Promise<string>} Resolved with the absolute path to the saved file
|
|
||||||
*/
|
*/
|
||||||
function downloadFile(url, filename) {
|
function downloadFile(url, filename) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
try {
|
try {
|
||||||
var https = require('https');
|
var https = require('https');
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
|
|
||||||
var tempPath = path.join(os.tmpdir(), filename);
|
var tempPath = path.join(os.tmpdir(), filename);
|
||||||
var file = fs.createWriteStream(tempPath);
|
var file = fs.createWriteStream(tempPath);
|
||||||
|
|
@ -517,15 +505,10 @@ function downloadFile(url, filename) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses csInterface.evalScript to call the Premiere Pro ExtendScript layer
|
* Calls csInterface.evalScript to import a local file into the open Premiere project.
|
||||||
* and import the given local file into the active project.
|
|
||||||
*
|
|
||||||
* @param {string} filePath - Absolute local path to the downloaded proxy file
|
|
||||||
* @returns {Promise<object>}
|
|
||||||
*/
|
*/
|
||||||
function importFileToPremiereProject(filePath) {
|
function importFileToPremiereProject(filePath) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
// Escape backslashes for Windows paths inside the script string literal
|
|
||||||
var safePath = filePath.replace(/\\/g, '\\\\');
|
var safePath = filePath.replace(/\\/g, '\\\\');
|
||||||
|
|
||||||
var script = [
|
var script = [
|
||||||
|
|
@ -577,7 +560,7 @@ function handleAssetClick(e) {
|
||||||
card.classList.add('selected');
|
card.classList.add('selected');
|
||||||
|
|
||||||
var assetId = card.dataset.assetId;
|
var assetId = card.dataset.assetId;
|
||||||
var asset = state.assets.find(function (a) { return a.id === assetId; });
|
var asset = state.assets.find(function (a) { return a.id === assetId; });
|
||||||
if (asset) showAssetDetails(asset);
|
if (asset) showAssetDetails(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -607,10 +590,10 @@ function showSuccessMessage(message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showFlash(message, className) {
|
function _showFlash(message, className) {
|
||||||
var el = document.createElement('div');
|
var el = document.createElement('div');
|
||||||
el.className = className;
|
el.className = className;
|
||||||
el.textContent = message;
|
el.textContent = message;
|
||||||
var anchor = document.querySelector('.search-filter-area');
|
var anchor = document.querySelector('.search-filter-area');
|
||||||
anchor.insertBefore(el, anchor.firstChild);
|
anchor.insertBefore(el, anchor.firstChild);
|
||||||
setTimeout(function () { el.remove(); }, 5000);
|
setTimeout(function () { el.remove(); }, 5000);
|
||||||
}
|
}
|
||||||
|
|
@ -641,9 +624,6 @@ function escapeHtml(str) {
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips characters illegal in file names on Windows/macOS.
|
|
||||||
*/
|
|
||||||
function sanitizeFilename(name) {
|
function sanitizeFilename(name) {
|
||||||
return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
|
return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
|
||||||
}
|
}
|
||||||
|
|
@ -653,9 +633,7 @@ function formatDuration(seconds) {
|
||||||
var h = Math.floor(seconds / 3600);
|
var h = Math.floor(seconds / 3600);
|
||||||
var m = Math.floor((seconds % 3600) / 60);
|
var m = Math.floor((seconds % 3600) / 60);
|
||||||
var s = Math.floor(seconds % 60);
|
var s = Math.floor(seconds % 60);
|
||||||
if (h > 0) {
|
if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
|
||||||
return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
|
|
||||||
}
|
|
||||||
return m + ':' + String(s).padStart(2, '0');
|
return m + ':' + String(s).padStart(2, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue