mirror of
https://gitlab.com/timvisee/send.git
synced 2024-11-13 06:32:34 +01:00
Merge pull request #502 from mozilla/refactor-filelist
extracted filelist into its own file
This commit is contained in:
commit
8d26e0e742
221
frontend/src/fileList.js
Normal file
221
frontend/src/fileList.js
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import FileSender from './fileSender';
|
||||||
|
import Storage from './storage';
|
||||||
|
import * as metrics from './metrics';
|
||||||
|
import { allowedCopy, copyToClipboard, ONE_DAY_IN_MS } from './utils';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
const storage = new Storage();
|
||||||
|
let fileList = null;
|
||||||
|
let $link = null;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
$link = $('#link');
|
||||||
|
fileList = document.getElementById('file-list');
|
||||||
|
toggleHeader();
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
for (let file of storage.files) {
|
||||||
|
const id = file.fileId;
|
||||||
|
checkExistence(id).then(exists => {
|
||||||
|
if (exists) {
|
||||||
|
addFile(storage.getFileById(id));
|
||||||
|
} else {
|
||||||
|
storage.remove(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleHeader() {
|
||||||
|
fileList.hidden = storage.files.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFile(file) {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const name = document.createElement('td');
|
||||||
|
const link = document.createElement('td');
|
||||||
|
const $copyIcon = $('<img>', {
|
||||||
|
src: '/resources/copy-16.svg',
|
||||||
|
class: 'icon-copy',
|
||||||
|
'data-l10n-id': 'copyUrlHover',
|
||||||
|
disabled: !allowedCopy()
|
||||||
|
});
|
||||||
|
const expiry = document.createElement('td');
|
||||||
|
const del = document.createElement('td');
|
||||||
|
const $delIcon = $('<img>', {
|
||||||
|
src: '/resources/close-16.svg',
|
||||||
|
class: 'icon-delete',
|
||||||
|
'data-l10n-id': 'deleteButtonHover'
|
||||||
|
});
|
||||||
|
const popupDiv = document.createElement('div');
|
||||||
|
const $popupText = $('<div>', { class: 'popuptext' });
|
||||||
|
const cellText = document.createTextNode(file.name);
|
||||||
|
|
||||||
|
const url = file.url.trim() + `#${file.secretKey}`.trim();
|
||||||
|
|
||||||
|
$link.attr('value', url);
|
||||||
|
$('#copy-text')
|
||||||
|
.attr('data-l10n-args', `{"filename": "${file.name}"}`)
|
||||||
|
.attr('data-l10n-id', 'copyUrlFormLabelWithName');
|
||||||
|
|
||||||
|
$popupText.attr('tabindex', '-1');
|
||||||
|
|
||||||
|
name.appendChild(cellText);
|
||||||
|
|
||||||
|
// create delete button
|
||||||
|
|
||||||
|
const delSpan = document.createElement('span');
|
||||||
|
$(delSpan)
|
||||||
|
.addClass('icon-cancel-1')
|
||||||
|
.attr('data-l10n-id', 'deleteButtonHover');
|
||||||
|
del.appendChild(delSpan);
|
||||||
|
|
||||||
|
const linkSpan = document.createElement('span');
|
||||||
|
$(linkSpan).addClass('icon-docs').attr('data-l10n-id', 'copyUrlHover');
|
||||||
|
|
||||||
|
link.appendChild(linkSpan);
|
||||||
|
link.style.color = '#0A8DFF';
|
||||||
|
|
||||||
|
//copy link to clipboard when icon clicked
|
||||||
|
$copyIcon.on('click', () => {
|
||||||
|
// record copied event from upload list
|
||||||
|
metrics.copiedLink({ location: 'upload-list' });
|
||||||
|
copyToClipboard(url);
|
||||||
|
document.l10n.formatValue('copiedUrl').then(translated => {
|
||||||
|
link.innerHTML = translated;
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
const linkImg = document.createElement('img');
|
||||||
|
$(linkImg)
|
||||||
|
.addClass('icon-copy')
|
||||||
|
.attr('data-l10n-id', 'copyUrlHover')
|
||||||
|
.attr('src', '/resources/copy-16.svg');
|
||||||
|
|
||||||
|
$(link).html(linkImg);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
file.creationDate = new Date(file.creationDate);
|
||||||
|
|
||||||
|
const future = new Date();
|
||||||
|
future.setTime(file.creationDate.getTime() + file.expiry);
|
||||||
|
|
||||||
|
let countdown = 0;
|
||||||
|
countdown = future.getTime() - Date.now();
|
||||||
|
let minutes = Math.floor(countdown / 1000 / 60);
|
||||||
|
let hours = Math.floor(minutes / 60);
|
||||||
|
let seconds = Math.floor(countdown / 1000 % 60);
|
||||||
|
|
||||||
|
const poll = () => {
|
||||||
|
countdown = future.getTime() - Date.now();
|
||||||
|
minutes = Math.floor(countdown / 1000 / 60);
|
||||||
|
hours = Math.floor(minutes / 60);
|
||||||
|
seconds = Math.floor(countdown / 1000 % 60);
|
||||||
|
let t;
|
||||||
|
|
||||||
|
if (hours >= 1) {
|
||||||
|
expiry.innerHTML = hours + 'h ' + minutes % 60 + 'm';
|
||||||
|
t = setTimeout(() => {
|
||||||
|
poll();
|
||||||
|
}, 60000);
|
||||||
|
} else if (hours === 0) {
|
||||||
|
expiry.innerHTML = minutes + 'm ' + seconds + 's';
|
||||||
|
t = window.setTimeout(() => {
|
||||||
|
poll();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
//remove from list when expired
|
||||||
|
if (countdown <= 0) {
|
||||||
|
storage.remove(file.fileId);
|
||||||
|
$(expiry).parents('tr').remove();
|
||||||
|
window.clearTimeout(t);
|
||||||
|
toggleHeader();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
poll();
|
||||||
|
|
||||||
|
// create popup
|
||||||
|
popupDiv.classList.add('popup');
|
||||||
|
const $popupMessage = $('<div>', { class: 'popup-message' });
|
||||||
|
$popupMessage.attr('data-l10n-id', 'deletePopupText');
|
||||||
|
const $popupAction = $('<div>', { class: 'popup-action' });
|
||||||
|
const $popupNvmSpan = $('<span>', { class: 'popup-no' });
|
||||||
|
$popupNvmSpan.attr('data-l10n-id', 'deletePopupCancel');
|
||||||
|
const $popupDelSpan = $('<span>', { class: 'popup-yes' });
|
||||||
|
$popupDelSpan.attr('data-l10n-id', 'deletePopupYes');
|
||||||
|
|
||||||
|
$popupText.html([$popupMessage, $popupAction]);
|
||||||
|
$popupAction.html([$popupNvmSpan, $popupDelSpan]);
|
||||||
|
|
||||||
|
// add data cells to table row
|
||||||
|
row.appendChild(name);
|
||||||
|
$(link).append($copyIcon);
|
||||||
|
row.appendChild(link);
|
||||||
|
row.appendChild(expiry);
|
||||||
|
$(popupDiv).append($popupText);
|
||||||
|
$(del).append($delIcon);
|
||||||
|
del.appendChild(popupDiv);
|
||||||
|
row.appendChild(del);
|
||||||
|
$('tbody').append(row); //add row to table
|
||||||
|
|
||||||
|
// delete file
|
||||||
|
$popupText.find('.popup-yes').on('click', e => {
|
||||||
|
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
||||||
|
$(e.target).parents('tr').remove();
|
||||||
|
const ttl = ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
||||||
|
metrics
|
||||||
|
.deletedUpload({
|
||||||
|
size: file.size,
|
||||||
|
time: file.totalTime,
|
||||||
|
speed: file.uploadSpeed,
|
||||||
|
type: file.typeOfUpload,
|
||||||
|
location: 'upload-list',
|
||||||
|
ttl
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
storage.remove(file.fileId);
|
||||||
|
});
|
||||||
|
toggleHeader();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// show popup
|
||||||
|
$delIcon.on('click', () => {
|
||||||
|
$popupText.addClass('show').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// hide popup
|
||||||
|
$popupText.find('.popup-no').on('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
$popupText.removeClass('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$popupText.on('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
//close when popup loses focus
|
||||||
|
$popupText.on('blur', () => {
|
||||||
|
$popupText.removeClass('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkExistence(id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
resolve(xhr.status === 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.open('get', '/exists/' + id);
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { addFile };
|
@ -86,7 +86,11 @@ export default class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFileById(id) {
|
getFileById(id) {
|
||||||
return this.engine.getItem(id);
|
try {
|
||||||
|
return JSON.parse(this.engine.getItem(id));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(property) {
|
remove(property) {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { Raven } from './common';
|
import { Raven } from './common';
|
||||||
import FileSender from './fileSender';
|
import FileSender from './fileSender';
|
||||||
import {
|
import {
|
||||||
|
allowedCopy,
|
||||||
bytes,
|
bytes,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
notify,
|
notify,
|
||||||
@ -11,15 +12,11 @@ import {
|
|||||||
import Storage from './storage';
|
import Storage from './storage';
|
||||||
import * as metrics from './metrics';
|
import * as metrics from './metrics';
|
||||||
import * as progress from './progress';
|
import * as progress from './progress';
|
||||||
|
import * as fileList from './fileList';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
const storage = new Storage();
|
const storage = new Storage();
|
||||||
|
|
||||||
const allowedCopy = () => {
|
|
||||||
const support = !!document.queryCommandSupported;
|
|
||||||
return support ? document.queryCommandSupported('copy') : false;
|
|
||||||
};
|
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
gcmCompliant()
|
gcmCompliant()
|
||||||
.then(function() {
|
.then(function() {
|
||||||
@ -29,7 +26,6 @@ $(() => {
|
|||||||
const $uploadWindow = $('.upload-window');
|
const $uploadWindow = $('.upload-window');
|
||||||
const $uploadError = $('#upload-error');
|
const $uploadError = $('#upload-error');
|
||||||
const $uploadProgress = $('#upload-progress');
|
const $uploadProgress = $('#upload-progress');
|
||||||
const $fileList = $('#file-list');
|
|
||||||
|
|
||||||
$pageOne.removeAttr('hidden');
|
$pageOne.removeAttr('hidden');
|
||||||
$('#file-upload').on('change', onUpload);
|
$('#file-upload').on('change', onUpload);
|
||||||
@ -44,27 +40,6 @@ $(() => {
|
|||||||
|
|
||||||
$link.attr('disabled', false);
|
$link.attr('disabled', false);
|
||||||
|
|
||||||
const toggleHeader = () => {
|
|
||||||
//hide table header if empty list
|
|
||||||
if (document.querySelector('tbody').childNodes.length === 1) {
|
|
||||||
$fileList.attr('hidden', true);
|
|
||||||
} else {
|
|
||||||
$fileList.removeAttr('hidden');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const files = storage.files;
|
|
||||||
if (files.length === 0) {
|
|
||||||
toggleHeader();
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line prefer-const
|
|
||||||
for (let index in files) {
|
|
||||||
const id = files[index].fileId;
|
|
||||||
//check if file still exists before adding to list
|
|
||||||
checkExistence(id, files[index], true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy link to clipboard
|
// copy link to clipboard
|
||||||
$copyBtn.on('click', () => {
|
$copyBtn.on('click', () => {
|
||||||
if (allowedCopy() && copyToClipboard($link.attr('value'))) {
|
if (allowedCopy() && copyToClipboard($link.attr('value'))) {
|
||||||
@ -243,7 +218,7 @@ $(() => {
|
|||||||
$uploadError.attr('hidden', true);
|
$uploadError.attr('hidden', true);
|
||||||
$('#share-link').removeAttr('hidden');
|
$('#share-link').removeAttr('hidden');
|
||||||
|
|
||||||
populateFileList(fileData);
|
fileList.addFile(fileData);
|
||||||
document.l10n.formatValue('notifyUploadDone').then(str => {
|
document.l10n.formatValue('notifyUploadDone').then(str => {
|
||||||
notify(str);
|
notify(str);
|
||||||
});
|
});
|
||||||
@ -272,201 +247,6 @@ $(() => {
|
|||||||
function allowDrop(ev) {
|
function allowDrop(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkExistence(id, file, populate) {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
if (populate) {
|
|
||||||
populateFileList(file);
|
|
||||||
}
|
|
||||||
} else if (xhr.status === 404) {
|
|
||||||
storage.remove(id);
|
|
||||||
if (storage.numFiles === 0) {
|
|
||||||
toggleHeader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.open('get', '/exists/' + id, true);
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
//update file table with current files in storage
|
|
||||||
const populateFileList = file => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
const name = document.createElement('td');
|
|
||||||
const link = document.createElement('td');
|
|
||||||
const $copyIcon = $('<img>', {
|
|
||||||
src: '/resources/copy-16.svg',
|
|
||||||
class: 'icon-copy',
|
|
||||||
'data-l10n-id': 'copyUrlHover',
|
|
||||||
disabled: !allowedCopy()
|
|
||||||
});
|
|
||||||
const expiry = document.createElement('td');
|
|
||||||
const del = document.createElement('td');
|
|
||||||
const $delIcon = $('<img>', {
|
|
||||||
src: '/resources/close-16.svg',
|
|
||||||
class: 'icon-delete',
|
|
||||||
'data-l10n-id': 'deleteButtonHover'
|
|
||||||
});
|
|
||||||
const popupDiv = document.createElement('div');
|
|
||||||
const $popupText = $('<div>', { class: 'popuptext' });
|
|
||||||
const cellText = document.createTextNode(file.name);
|
|
||||||
|
|
||||||
const url = file.url.trim() + `#${file.secretKey}`.trim();
|
|
||||||
|
|
||||||
$link.attr('value', url);
|
|
||||||
$('#copy-text')
|
|
||||||
.attr('data-l10n-args', JSON.stringify({ filename: file.name }))
|
|
||||||
.attr('data-l10n-id', 'copyUrlFormLabelWithName');
|
|
||||||
|
|
||||||
$popupText.attr('tabindex', '-1');
|
|
||||||
|
|
||||||
name.appendChild(cellText);
|
|
||||||
|
|
||||||
// create delete button
|
|
||||||
|
|
||||||
const delSpan = document.createElement('span');
|
|
||||||
$(delSpan)
|
|
||||||
.addClass('icon-cancel-1')
|
|
||||||
.attr('data-l10n-id', 'deleteButtonHover');
|
|
||||||
del.appendChild(delSpan);
|
|
||||||
|
|
||||||
const linkSpan = document.createElement('span');
|
|
||||||
$(linkSpan).addClass('icon-docs').attr('data-l10n-id', 'copyUrlHover');
|
|
||||||
|
|
||||||
link.appendChild(linkSpan);
|
|
||||||
link.style.color = '#0A8DFF';
|
|
||||||
|
|
||||||
//copy link to clipboard when icon clicked
|
|
||||||
$copyIcon.on('click', () => {
|
|
||||||
// record copied event from upload list
|
|
||||||
metrics.copiedLink({ location: 'upload-list' });
|
|
||||||
copyToClipboard(url);
|
|
||||||
document.l10n.formatValue('copiedUrl').then(translated => {
|
|
||||||
link.innerHTML = translated;
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
const linkImg = document.createElement('img');
|
|
||||||
$(linkImg)
|
|
||||||
.addClass('icon-copy')
|
|
||||||
.attr('data-l10n-id', 'copyUrlHover')
|
|
||||||
.attr('src', '/resources/copy-16.svg');
|
|
||||||
|
|
||||||
$(link).html(linkImg);
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
file.creationDate = new Date(file.creationDate);
|
|
||||||
|
|
||||||
const future = new Date();
|
|
||||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
|
||||||
|
|
||||||
let countdown = 0;
|
|
||||||
countdown = future.getTime() - Date.now();
|
|
||||||
let minutes = Math.floor(countdown / 1000 / 60);
|
|
||||||
let hours = Math.floor(minutes / 60);
|
|
||||||
let seconds = Math.floor(countdown / 1000 % 60);
|
|
||||||
|
|
||||||
const poll = () => {
|
|
||||||
countdown = future.getTime() - Date.now();
|
|
||||||
minutes = Math.floor(countdown / 1000 / 60);
|
|
||||||
hours = Math.floor(minutes / 60);
|
|
||||||
seconds = Math.floor(countdown / 1000 % 60);
|
|
||||||
let t;
|
|
||||||
|
|
||||||
if (hours >= 1) {
|
|
||||||
expiry.innerHTML = hours + 'h ' + minutes % 60 + 'm';
|
|
||||||
t = setTimeout(() => {
|
|
||||||
poll();
|
|
||||||
}, 60000);
|
|
||||||
} else if (hours === 0) {
|
|
||||||
expiry.innerHTML = minutes + 'm ' + seconds + 's';
|
|
||||||
t = window.setTimeout(() => {
|
|
||||||
poll();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
//remove from list when expired
|
|
||||||
if (countdown <= 0) {
|
|
||||||
storage.remove(file.fileId);
|
|
||||||
$(expiry).parents('tr').remove();
|
|
||||||
window.clearTimeout(t);
|
|
||||||
toggleHeader();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
poll();
|
|
||||||
|
|
||||||
// create popup
|
|
||||||
popupDiv.classList.add('popup');
|
|
||||||
const $popupMessage = $('<div>', { class: 'popup-message' });
|
|
||||||
$popupMessage.attr('data-l10n-id', 'deletePopupText');
|
|
||||||
const $popupAction = $('<div>', { class: 'popup-action' });
|
|
||||||
const $popupNvmSpan = $('<span>', { class: 'popup-no' });
|
|
||||||
$popupNvmSpan.attr('data-l10n-id', 'deletePopupCancel');
|
|
||||||
const $popupDelSpan = $('<span>', { class: 'popup-yes' });
|
|
||||||
$popupDelSpan.attr('data-l10n-id', 'deletePopupYes');
|
|
||||||
|
|
||||||
$popupText.html([$popupMessage, $popupAction]);
|
|
||||||
$popupAction.html([$popupNvmSpan, $popupDelSpan]);
|
|
||||||
|
|
||||||
// add data cells to table row
|
|
||||||
row.appendChild(name);
|
|
||||||
$(link).append($copyIcon);
|
|
||||||
row.appendChild(link);
|
|
||||||
row.appendChild(expiry);
|
|
||||||
$(popupDiv).append($popupText);
|
|
||||||
$(del).append($delIcon);
|
|
||||||
del.appendChild(popupDiv);
|
|
||||||
row.appendChild(del);
|
|
||||||
$('tbody').append(row); //add row to table
|
|
||||||
|
|
||||||
// delete file
|
|
||||||
$popupText.find('.popup-yes').on('click', e => {
|
|
||||||
FileSender.delete(file.fileId, file.deleteToken).then(() => {
|
|
||||||
$(e.target).parents('tr').remove();
|
|
||||||
const ttl =
|
|
||||||
ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
|
||||||
metrics
|
|
||||||
.deletedUpload({
|
|
||||||
size: file.size,
|
|
||||||
time: file.totalTime,
|
|
||||||
speed: file.uploadSpeed,
|
|
||||||
type: file.typeOfUpload,
|
|
||||||
location: 'upload-list',
|
|
||||||
ttl
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
storage.remove(file.fileId);
|
|
||||||
});
|
|
||||||
toggleHeader();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// show popup
|
|
||||||
$delIcon.on('click', () => {
|
|
||||||
$popupText.addClass('show').focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// hide popup
|
|
||||||
$popupText.find('.popup-no').on('click', e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
$popupText.removeClass('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$popupText.on('click', e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
//close when popup loses focus
|
|
||||||
$popupText.on('blur', () => {
|
|
||||||
$popupText.removeClass('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
toggleHeader();
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
metrics.unsupported({ err }).then(() => {
|
metrics.unsupported({ err }).then(() => {
|
||||||
|
@ -129,9 +129,15 @@ function percent(ratio) {
|
|||||||
: `${Math.floor(ratio * 100)}%`;
|
: `${Math.floor(ratio * 100)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function allowedCopy() {
|
||||||
|
const support = !!document.queryCommandSupported;
|
||||||
|
return support ? document.queryCommandSupported('copy') : false;
|
||||||
|
}
|
||||||
|
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
const ONE_DAY_IN_MS = 86400000;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
allowedCopy,
|
||||||
bytes,
|
bytes,
|
||||||
percent,
|
percent,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
|
Loading…
Reference in New Issue
Block a user