import { api as api_type } from 'app/types' // import { format, formatDistance } from 'date-fns' import format from 'date-fns/format' import formatDistance from 'date-fns/formatDistance' export const formatDateTime = dateStr => format(new Date(dateStr), 'd MMM yyyy H:mm') export const formatDate = dateStr => format(new Date(dateStr), 'd MMM yyyy') export const formatTime = dateStr => format(new Date(dateStr), 'H:mm') export const formatAge = dateStr => formatDistance(new Date(), new Date(dateStr)) + ' ago.' export const toArray = a => Array.from(a) /* Mobile check */ export const isiPhone = !!((navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i))) export const isiPad = !!(navigator.userAgent.match(/iPad/i)) export const isAndroid = !!(navigator.userAgent.match(/Android/i)) export const isMobile = isiPhone || isiPad || isAndroid export const isHandheld = isiPhone || (isAndroid && window.innerWidth <= 760) export const isTablet = isMobile && window.innerWidth > 760 export const isDesktop = !isMobile const htmlClassList = document.body.parentNode.classList htmlClassList.add(isDesktop ? 'desktop' : 'mobile') /* Default image dimensions */ export const widths = { th: 160, sm: 320, md: 640, lg: 1280, } /* Formatting functions */ const acronyms = 'id url cc sa fp md5 sha256'.split(' ').map(s => '_' + s) const acronymsUpperCase = acronyms.map(s => s.toUpperCase()) export const formatName = s => { acronyms.forEach((acronym, i) => s = s.replace(acronym, acronymsUpperCase[i])) return s.replace(/_/g, ' ') } // Use to pad frame numbers with zeroes export const pad = (n, m) => { let s = String(n || 0) while (s.length < m) { s = '0' + s } return s } export const courtesyS = (n, s) => n + ' ' + (n === 1 ? s : s + 's') export const capitalize = s => s.split(' ').map(capitalizeWord).join(' ') export const capitalizeWord = s => s.substr(0, 1).toUpperCase() + s.substr(1) export const padSeconds = n => n < 10 ? '0' + n : n export const timestamp = (n = 0, fps = 1, ms = false, h_label = ':', m_label = ':', s_label = '') => { if (n < 0) { return '0' + m_label + '00' + s_label } let s = '' n /= fps if (ms) { const mantissa = Math.floor((n % 1) * 10) s = '.' + mantissa } n = Math.floor(n) s = padSeconds(n % 60) + s n = Math.floor(n / 60) if (n > 60) { return Math.floor(n / 60) + h_label + padSeconds(n % 60) + m_label + s + s_label } return (n % 60) + m_label + s + s_label } export const timestampHMS = n => timestamp(n, 1, false, 'h', 'm', 's') export const timestampToSeconds = time_str => { const time_str_parts = (time_str || "").trim().split(":").map(s => parseFloat(s)) if (time_str_parts.length === 3) { return (time_str_parts[0] * 60 + time_str_parts[1]) * 60 + time_str_parts[2] } if (time_str_parts.length === 2) { return time_str_parts[0] * 60 + time_str_parts[1] } return time_str_parts[0] } export const percent = n => (n * 100).toFixed(1) + '%' export const px = (n, w) => Math.round(n * w) + 'px' export const clamp = (n, a=0, b=1) => n < a ? a : n < b ? n : b export const dist = (x1, y1, x2, y2) => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) export const xor = (a,b) => ((!a && !!b) || (!!a && !b)) export const mod = (n, m) => n - (m * Math.floor(n / m)) export const angle = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1) export const lerp = (n,a,b) => (b-a)*n+a export const floatEQ = (a,b) => ((a*10|0) === (b*10|0)) export const floatLT = (a,b) => ((a*10|0) < (b*10|0)) export const floatLTE = (a,b) => (floatEQ(a,b) || floatLT(a,b)) export const floatGT = (a,b) => ((a*10|0) > (b*10|0)) export const floatGTE = (a,b) => (floatEQ(a,b) || floatGT(a,b)) export const floatInRange = (a,b,c) => floatLTE(a, b) && floatLT(b, c) export const simpleArraysEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b) /* forms */ export const mapNameToSelectOption = labels => ( name => ({ name, label: labels[name] || capitalize(name.replace('_', ' ')) }) ) /* URLs */ export const sha256_tree = (sha256, branch_size=2, tree_depth=2) => { const tree_size = tree_depth * branch_size let tree = "" for (var i = 0; i < tree_size; i += branch_size) { tree += '/' + sha256.substr(i, branch_size) } return tree } export const urlSearchParamsToDict = search => { const params = new URLSearchParams(search) const dict = {} params.forEach((value, key) => { // ??? dict[key] = value }) return dict } /* AJAX */ let cachedAuth = null let token = '' let username = '' export const post = (dispatch, type=api_type, tag, url, data) => { let headers if (data instanceof FormData) { headers = { Accept: 'application/json', } } else if (data) { headers = { Accept: 'application/json', 'Content-Type': 'application/json; charset=utf-8', } data = JSON.stringify(data) } dispatch({ type: type.loading, tag, }) return fetch(url, { method: 'POST', body: data, headers, }) .then(res => res.json()) .then(res => { dispatch({ type: type.loaded, tag, data: res, }) return res }) .catch(err => { dispatch({ type: type.error, tag, err, }) return err }) } export const api = (dispatch, type=api_type, tag, url, data) => { dispatch({ type: type.loading, tag, }) if (url.indexOf('http') !== 0) { url = window.location.origin + url } url = new URL(url) if (data) { url.search = new URLSearchParams(data).toString() } let req = fetch(url, { method: 'GET', // mode: 'cors', }) // console.log(tag) if (tag === 'text') { req = req.then(res => res.text()) } else { req = req.then(res => res.json()) } req = req.then(res => { dispatch({ type: type.loaded, tag, data: res, }) return res }) .catch(err => dispatch({ type: type.error, tag, err, })) return req } /* sorting */ export const numericSort = { asc: (a,b) => a[0] - b[0], desc: (a,b) => b[0] - a[0], } export const stringSort = { asc: (a,b) => a[0].localeCompare(b[0]), desc: (a,b) => b[0].localeCompare(a[0]), } export const orderByFn = (s='name asc') => { const [field='name', direction='asc'] = s.split(' ') let mapFn, sortFn switch (field) { case 'id': mapFn = a => [parseInt(a.id) || 0, a] sortFn = numericSort[direction] break case 'epoch': mapFn = a => [parseInt(a.epoch || a.epochs) || 0, a] sortFn = numericSort[direction] break case 'size': mapFn = a => [parseInt(a.size) || 0, a] sortFn = numericSort[direction] break case 'start_ts': mapFn = a => [parseFloat(a.start_ts) || 0, a] sortFn = numericSort[direction] break case 'date': mapFn = a => [+new Date(a.date || a.created_at), a] sortFn = numericSort[direction] break case 'updated_at': mapFn = a => [+new Date(a.updated_at), a] sortFn = numericSort[direction] break case 'priority': mapFn = a => [parseInt(a.priority) || parseInt(a.id) || 1000, a] sortFn = numericSort[direction] break case 'title': mapFn = a => [a.title || "", a] sortFn = stringSort[direction] break case 'author': mapFn = a => { let author = (a.author || "").split(' and ')[0].split(' ') author.unshift(author.pop()) author = author.join(' ') return [author, a] } sortFn = stringSort[direction] break case 'name': default: mapFn = a => [a.name || "", a] sortFn = stringSort[direction] break } return { mapFn, sortFn } } export const getOrderedIds = (objects, sort, prepend) => { if (!prepend) { prepend = [] } const { mapFn, sortFn } = orderByFn(sort) return prepend.concat(objects.map(mapFn).sort(sortFn).map(a => a[1].id)) } export const getOrderedIdsFromLookup = (lookup, sort) => { return getOrderedIds(Object.keys(lookup).map(key => lookup[key]), sort) } /* parallel promises */ export const allProgress = (promises, progress_cb) => { let d = 0 progress_cb(0, 0, promises.length) promises.forEach((p) => { p.then((s) => { d += 1 progress_cb(Math.floor((d * 100) / promises.length), d, promises.length) return s }) }) return Promise.all(promises) } /* Clipboard */ export function writeToClipboard(str) { return new Promise((resolve, reject) => { let success = false function listener(e) { e.clipboardData.setData("text/plain", str) e.preventDefault() success = true } document.addEventListener("copy", listener) document.execCommand("copy") document.removeEventListener("copy", listener) if (success) { resolve() } else { reject() } }) }